.Net client authentication setup using HttpWebRequest - c#

This is more about how to get HttpWebRequest to work or even if HttpWebRequest is the right implementation. I've let my C# and .Net skill lapse the past few year, so I hope I can be forgiven for that.
I trying to hit a secure web service that requires client authentication. I have four certs to hit this with.
• Root Certificate
• Intermediate Root Certificate
• Device Certificate
• Private Key
The server is Java and these certs are in .jks form trustore and keystore. I pulled them into .pem files.
So, I failed on the C# client side, so I thought I'd write a little Python snippet to make sure at least the server side is working as expected. Twenty minutes later, I'm making secure posts. Here's that code:
# Keys
path = "C:\\path\\"
key = path + "device.pem"
privkey = path + "device_privkey.pem"
CACerts = path + "truststore.concat" # root & intermediate cert
def post():
url = "/url"
headers = {'Content-Type': 'application/xml'}
## This section is HTTPSConnection
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.verify_mode = ssl.CERT_OPTIONAL
context.load_cert_chain(key, privkey, password='password')
context.verify_mode = ssl.CERT_NONE
context.load_verify_locations(CACerts)
conn = http.client.HTTPSConnection(host, port=8080, context=context)
conn.request("POST", url, registrationBody, headers)
response = conn.getresponse()
regresp = response.read()
The concat certificate is the concatenation of the root and intermediate certificates.
Are you with me?
Now to my C#/.Net headache.
This my attempt. I clearly don't know what I'm doing here.
public async Task POSTSecure(string pathname, string body)
{
string path = "C:\\path";
string key = path + "device.pem";
string privkey = path + "device_privkey.pem";
string CACerts1 = path + "vtn_root.pem";
string CACerts2 = path + "vtn_int.pem";
try
{
// Create certs from files
X509Certificate2 keyCert = new X509Certificate2(key);
X509Certificate2 rootCert = new X509Certificate2(CACerts1);
X509Certificate2 intCert = new X509Certificate2(CACerts2);
HttpWebRequest request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create("https://" + host + ":" + port + pathname);
ServicePoint currentServicePoint = request.ServicePoint;
// build the client chain?
request.ClientCertificates.Add(keyCert);
request.ClientCertificates.Add(rootCert);
request.ClientCertificates.Add(intCert);
Console.WriteLine("URI: {0}", currentServicePoint.Address);
// This validates the server regardless of whether it should
request.ServerCertificateValidationCallback = ValidateServerCertificate;
request.Method = "POST";
request.ContentType = "application/xml";
request.ContentLength = body.Length;
using (var sendStream = request.GetRequestStream())
{
sendStream.Write(Encoding.UTF8.GetBytes(body), 0, body.Length);
}
var response = (HttpWebResponse)request.GetResponse();
}
catch (Exception e)
{
Console.WriteLine("Post error.");
}
}
Thanks for any help or a pointer to a decent tutorial.
[Edit] More info. On the server side, the debugging points to an empty client certificate chain. This is right after it reports serverhello done.

Okay, I think I was pretty close in the original, but I solved it this way:
request.ClientCertificates = new X509Certificate2Collection(
new X509Certificate2(
truststore,
password));
The "trustore" file is a .p12 containing the certificates listed above. The .p12 truststore can be created from the .jks truststore through keytool and openssl. Lots of info out there on how to do that.

Related

FTP Download File generate the error: A call to SSPI failed

I am trying to download a file from my FTP Server but from my production server (from my laptop I am able to download the file) I get the error "A call to SSPI failed" and the inner exception "The message received was unexpected or badly formatted". This my code:
var ftpServer = "ftp://xxx.yyy.zz/";
var ftpUsername = "aaaaa";
var ftpPassword = "bbbbb";
var downloadedFilePath = "c:\\temp\\";
var downloadedFileName = "file.xls";
FtpWebRequest request = null;
ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = delegate (object s, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
return true;
};
// Get the object used to communicate with the server.
request = (FtpWebRequest)WebRequest.Create(ftpServer + "file_on_ftp_server.xls");
// download the file
request.Method = WebRequestMethods.Ftp.DownloadFile;
request.Credentials = new NetworkCredential(ftpUsername, ftpPassword);
request.EnableSsl = true;
request.KeepAlive = false;
FtpWebResponse response = (FtpWebResponse)request.GetResponse(); // <--- ERROR
Stream responseStream = response.GetResponseStream();
FileStream writer = new FileStream(downloadedFilePath + downloadedFileName, FileMode.Create);
SSPI is a Windows component.
If you are on Windows 2008, it means it's very old.
The error "The message received was unexpected or badly formatted" seems to indicate that the server certificate
uses an unsupported format (for instance, uses an unknown cipher), and thus can't be processed by the SSPI layer.
You have several solutions:
Upgrade Windows
Downgrade the server certificate
Don't use SSL
Use an alternative FTP library that does not rely on SSPI
To use SSL certificate inside .Net framework we need to provide both certificate and its corresponding private key together. To achieve this we need to use p12(.pfx) file which combined this two. In my project, I have used self-signed certificate using OpenSSL so I used below command to combine certificate and private key
pkcs12 -export -out ca.pfx -inkey ca.key -in ca.crt
pkcs12 -export -out client.pfx -inkey client.key -in client.crt
which will create p12(.pfx) file for each certificate. Then used them into your code like below .
see below lonk for more information
Reference
maybe this link also help you .
Good luck
Based on the previous suggestions your code should look like this:
var ftpServer = "ftp://xxx.yyy.zz/";
var ftpUsername = "aaaaa";
var ftpPassword = "bbbbb";
var downloadedFilePath = "c:\\temp\\";
var downloadedFileName = "file.xls";
var certName = "MyCertName" //Use yours
var password = "MyCertPassword"; //Use yours
FtpWebRequest request = null;
ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = delegate (object s, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
return true;
};
//Load certificates (one or more)
X509Certificate2Collection certificates = new X509Certificate2Collection();
certificates.Import(certName, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// Get the object used to communicate with the server.
request = (FtpWebRequest)WebRequest.Create(ftpServer + "file_on_ftp_server.xls");
// download the file
request.Method = WebRequestMethods.Ftp.DownloadFile;
request.Credentials = new NetworkCredential(ftpUsername, ftpPassword);
request.ClientCertificates = certificates; //Use certificates
request.EnableSsl = true;
request.KeepAlive = false;
FtpWebResponse response = (FtpWebResponse)request.GetResponse(); // <--- ERROR
Stream responseStream = response.GetResponseStream();
FileStream writer = new FileStream(downloadedFilePath + downloadedFileName, FileMode.Create);
Note: I have not tested the solution because I don't have an environment to test it on
The TLS handshake was failing because two servers were unable to agree on a common cipher suite.
IIS Crypto to enable additional cipher suites on web app's server. downloaded and ran IIS Crypto, checkmarked additional cipher suites on its Cipher Suites tab after then restarted the machine.
If diagnose failure, I'd recommend installing Wireshark on the machine with your .NET Core app.
If TLS Handshake Failure again,a message like: Alert (Level: Fatal, Description: Handshake Failure)
Some cipher suites are more secure than others, so check ref link may help to solve your problem.
temporary solution:
set the minimum key length for schannel using windows registry:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\KeyExchangeAlgorithms\Diffie-Hellman]
"ClientMinKeyBitLength"=dword:00000200
Ref: Link1, link2, link3 ,Link4

Recreate Az.Connect-AzAccount in c#

PowerShell has a great function in the Az.Accounts library - "Connect-AzAccount" - that allows me to authenticate against my Azure instance and publish APIM's easily. I need to port the code over to c# and am unable to find an easy equivalent.
Has anyone found a straightforward way to do this? The Azure world / libraries seem to be updating frequently, along with the many security aspects. It's difficult to find a recent, relevant example.
My ultimate goal is to authenticate against Azure, then publish API's to the API Gateway.
The PowerShell code:
$user = "account#domain.com"
$password = ConvertTo-SecureString -String "password goes here" -AsPlainText -Force
$azAccount = #{
subscription = "subscription-guid-goes-here"
credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $user,$password
}
Connect-AzAccount #azAccount
Currently, Azure Management Library for .NET can only be used to manage partial Azure resources. However, Azure API Management is not included.
So, I suggest you use Azure REST API to manage your resources.
As Azure REST APi is protected under Azure AD. So, the first step is to acquire an accesstoken for authentication.
Here is a sample by using Microsoft.IdentityModel.Clients.ActiveDirectory package
static string GetToken()
{
string tenantId = "your tenant id or name, for example: hanxia.onmicrosoft.com";
string clientId = "1950a258-227b-4e31-a9cf-717495945fc2"; // it is a public client for every tenant.
string resource = "https://management.core.windows.net/";
string username = "user name, jack#hanxia.onmicrosoft.com";
string password = "password, D******";
var upc = new UserPasswordCredential(username, password);
var context = new AuthenticationContext("https://login.microsoftonline.com/" + tenantId);
AuthenticationResult result = context.AcquireTokenAsync(resource, clientId, upc).Result;
return result.AccessToken;
}
After that, you can call Azure REST API along with adding authorization header. Here is a POST request sample:
public static string postRequest(string url, string access_token, string data)
{
byte[] buffer = null;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "post";
request.ContentType = "application/json";
request.Headers.Add("Authorization", "Bearer " + access_token);
//request.Headers.Add("other header", "it's value");
if (data != null)
buffer = Encoding.UTF8.GetBytes(data);
else
buffer = Encoding.UTF8.GetBytes("");
request.ContentLength = buffer.Length;
request.GetRequestStream().Write(buffer, 0, buffer.Length);
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
{
return response.StatusCode + " " + reader.ReadToEnd();
}
}
the existing answer is a bit odd, its linking some old depreciated library. Official library supports "managing" Api Management. That would be the preferred way of managing Azure resources with C# and you dont have to recreate anything.

OpenSSL with C#

i am trying to write a program in C# through which i could be able to retrieve SSL Certificate Subject of a URL. For this purpose i have to used OpenSSL with TCP port, i have tried too many functions but not found any solution.
I have written the following code:
try {
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://google.com");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
response.Close();
var cert = request.ServicePoint.Certificate;
var subject = cert.Subject.ToString();
txt.Text = subject;
txt.Text = txt.Text + "<br>" + cert.Issuer;
}
catch (Exception exe)
{
txt.Text = exe.ToString();
}
This code returns me the correct result according to my requirement but it is not on OpenSSL.
I have found the following code which is the perfect for my requirement but it is in c language where i need the same in C#.
http://fm4dd.com/openssl/sslconnect.htm
Any help be appreciated.
thanks

Error: C# The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel

I'm trying to make a request via SSL. The certificate is already installed on the machine and it works via browser.
I am using this request:
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
byte[] data = encoding.GetBytes(request.Content.OuterXml.ToString());
string password = "XXXX";
X509Certificate2 cert = new X509Certificate2("c:\\zzzz.p12", password);
string key = cert.GetPublicKeyString();
string certData = Encoding.ASCII.GetString(cert.Export(X509ContentType.Cert));
Uri uri = new Uri(request.Url);
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(uri);
myRequest.Credentials = new NetworkCredential(request.User, request.Password.ToString());
myRequest.Method = "PUT";
myRequest.ContentType = request.ContentType;
myRequest.ContentLength = data.Length;
myRequest.ClientCertificates.Add(cert);
Stream newStream = myRequest.GetRequestStream();
newStream.Write(data, 0, data.Length);
newStream.Close();
System.IO.StreamReader st = new StreamReader(((HttpWebResponse)myRequest.GetResponse()).GetResponseStream());
Using this code I get this error:
The underlying connection was closed: Could not establish trust
relationship for the SSL/TLS secure channel. --->
System.Security.Authentication.AuthenticationException: The remote
certificate is invalid according to the validation procedure.
What is the problem?
I solved the problem with this:
ServicePointManager.ServerCertificateValidationCallback = new
RemoteCertificateValidationCallback
(
delegate { return true; }
);
Make sure your certificate is properly trusted. Has the root certificate been added to the correct certificate store (Trusted Root CA's on Local Machine)?
I encountered this error when the (own made) root certificate for a (self signed) certificate had been added to the Trusted Root CA's for Current User). Moving the root cert to the Root CA store on Local Machine solved my issue.
You can add the following statement in the function that calls the web-method:
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
This is client code, right? And you're accessing an HTTPS url, aren't you?
I think the problem is with the server certificate, which the client-side TLS stack cannot validate.
When you connect to that URL via browser, does it work smoothly or do you see a warning on certificate mismatch?

httpwebrequest using a pfx?

I'd like to reproduce the following chunk of vbscript using C#.
sURL = "https://server/service.dll"
sXML = "<request version=""1.0""><ticket>generated ticket</ticket></request>"
dim winHTTPReq: Set winHTTPReq = CreateObject("WinHttp.WinHttpRequest.5.1")
winHttpReq.Open "POST", sURL, false
winHTTPReq.SetRequestHeader "Content-Type", "application/x-some-type"
sCertificate = "LOCAL_MACHINE\MY\Vendor Supplied PFX"
winHTTPReq.SetClientCertificate sCertificate
WinHttpReq.Send(sXML)
....
All my attempts to do so have been a) non-functional and b) a bit longer than the snippet above.
var xml = (new XElement("request"
, new XElement("ticket", "generated ticket")).ToString();
var cert = X509Certificate.CreateFromCertFile("c:\\Exported Vendor Supplied PFX.cer");
ServicePointManager.ServerCertificateValidationCallback += delegate { return true; };
var request = (HttpWebRequest)WebRequest.Create("https://server/service.dll");
request.ClientCertificates.Add(cert);
request.ContentType = "application/x-some-type";
request.Method = "POST";
byte[] bytes = Encoding.UTF8.GetBytes(xml);
request.ContentLength = bytes.Length;
using (var requestStream = request.GetRequestStream())
requestStream.Write(bytes, 0, bytes.Length);
using (var response = (HttpWebResponse)request.GetResponse())
if (response.StatusCode != HttpStatusCode.OK) {
string message = String.Format(
"POST failed. Received HTTP {0}",
response.StatusCode);
throw new ApplicationException(message);
}
I consistently get a 403 with this latter code. I'm not sure why but I'm leaning towards permissions. Is there anything in particular that is required before I can create a certificate from a cer file? Do I need to apply permissions via winhttpcertcfg?
I was originally given a pfx which was imported using the Certificate MMC. There doesn't appear to be a way to add a pfx as a client certificate, consequently I've exported the pfx as a DER x509 cer for use with the CreateFromCertFile() method. Is there a way to only use the pfx?
Of course, the original VBScript works fine - I'd just like to get an idea of what it would take to convert it. It doesn't seem like it'd be too difficult but so far I've had a bit of a rough time.
[Update]
Apparently it's possible to use a pfx on disk (provided you have a password) however [this]http://support.microsoft.com/kb/948154 seems to suggest that it's better to rely on a certificate store. Since this is similar to what the original vbscript was doing I modified the code above to pull the cert from a store:
var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
X509Certificate2 cert = store.Certificates.Find(X509FindType.FindBySubjectName
, "XXXX", false)[0];
I added granted "Everyone" access to the cert via the WinHttpCertCfg tool. Still no luck. Unfortunately this also returns a 403 from the server. Walking though the code I can see that the certificate is being found and created successfully. I can also verify that it's added as a client certificate. For whatever reason I just haven't had any luck using HttpWebResponse.
However, using interop works fine. As much as I hate to rely on something not native to the framework, the WinHttpRequest component seems to be so much easier to use. I can accomplish the same thing with roughly 1/4th the code and this integrates fine with the rest of the app.
xml = "<request version=\"1.0\"><ticket>generated ticket</ticket></request>";
var req = new WinHttp.WinHttpRequest();
req.Open("POST", "https://server/service.dll", false);
req.SetRequestHeader("Content-Type", "application/x-some-type");
req.SetClientCertificate(#"LOCAL_MACHINE\MY\Vendor Supplied PFX");
req.Send(xml);
Console.WriteLine(string.Format("{0} {1}", req.Status, req.StatusText));
Console.WriteLine(req.ResponseText);

Categories