I have created a C# Web Server and I have some problems with the SSL/TLS certificates.
For the setup I have changed my /etc/hosts file for that any request to example.com will direct
to localhost. After that I have created a TCP server which works quite as expected. For the
HTTPS I added a System.Net.Security.SslStream layer with a default self-signed certificate which
also works as expected.
But for now I want to send a certificate that depends on the location the user requests. My server
code is like this:
var stream = new SslStream(
innerStream: client.GetStream(),
leaveInnerStreamOpen: false,
userCertificateValidationCallback: null,
userCertificateSelectionCallback: LocalCertificateSelection
);
stream.AuthenticateAsServer(
// I am forced to provide a default certificate. null will throw an error
serverCertificate: Settings.Certificate,
clientCertificateRequired: false,
enabledSslProtocols: SslProtocols.None,
checkCertificateRevocation: true
);
private X509Certificate LocalCertificateSelection(
object sender,
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers
)
{
// at this point: targetHost == null && acceptableIssuers.Length == 0
return Settings.Certificate;
};
But if I open the page in Firefox or in curl the targetHost and acceptableIssuers from my
certificate selection callback are always empty.
How am I supposed to get the host name to provide the correct certificate?
Edit 1:
My library is targeted for .Net Standard 2.1. Maybe this will help.
Edit 2:
I have added the certificate to the key store of the server (as it was mentioned in a comment), but this wont resolve the problem:
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(Settings.Certificate);
After that I have captured some packages with Wireshark and I have noticed the following:
After TCP connection is finished:
Client sends a TLS Hello request with a valid SNI extension (here is the host name I want to get)
Debugger break point in my code is called. The SslStream tries to get a valid certificate: targetHost and acceptableIssuers are empty.
Server sends the default certificate with possible invalid host name
Related
I'd like to ask for explanation. I am using X.509 certificate, and when I tried to post my data to a webservice which I want to communicate with, I am getting the following error: "
No client certificate was presented during SSL Handshake
can you please explain me what is the issue?
NB: I am using .NET Framework 3.5 / C#
What I did exactly is: First I imported the certificate into the store, then I used the code below in order to find it and then recieve the token (using AskForToken function). However, when I send with my data, I got handshake failure.
My first question is why I succeed to get token (if I am not mistaken, the client (which is my application) sent the certificate to the server and got the token, which means the connection has been done well)?
My second question, what do I have to change or check to get rid of this handshake failure.
private static string RequestSecurityToken()
{
WSTrustChannelFactory trustChannelFactory = new Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannelFactory(
new CertificateWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(new Uri(stsAddress)));
trustChannelFactory.TrustVersion = TrustVersion.WSTrust13;
string thumb = "fe14593dd66b2406c5269d742d04b6e1ab03adb1";
trustChannelFactory.Credentials.ClientCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindByThumbprint, thumb);
trustChannelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;
cert = trustChannelFactory.Credentials.ClientCertificate.Certificate;
var tokenString = AskForToken(serviceURL, trustChannelFactory);
trustChannelFactory.Close();
return tokenString;
}
SSL has a possibility to demand client authentification. So the Client (your application) has to send a certificate that the Server trusts before the connection is established. It seems that this client authentification fails, because your application doesn´t send such a certificate.
Depending on the webservice you try to use it won´t be possible to create such a certificate, because the server only trusts application from e.g. a certain company.
During an SSL handshake, the client presents it's public key to the other party, which apparently is not happening.
I'm not a C# programmer so I can't present you with the code. But you need to create an SSL keypair (private + public key pair) and use it to define your SSL sockets etc.
Can't solve the problem with certificate validation.
There's Web API server, that uses HTTPS to handle requests. Server's certificate has this certification path: RCA (root) -> ICA (intermediate) -> Web API server. RCA, ICA and Web API server are members of the same Active Directory domain.
Client application (desktop, computer is joined to the same domain) uses HttpClient to communicate with server and supports two scenarios:
connected to corporate network;
disconnected from corporate network (Internet access).
Both scenarios use basic authentication.
RCA and ICA certificates are placed in "Trusted Root Certification Authorities" and "Intermediate Certification Authorities" respectively for local computer account. RCA certificate is self-signed.
Now, when client is connected to corporate network, certificate validation works as expected, and user can "talk" to Web API.
When client is disconnected (only Internet connection is available), certificate validation fails with AuthenticationException ("The remote certificate is invalid according to the validation procedure").
I don't want to turn off certificate validation completely, but just need a way to tell validation system, that this particular certificate is valid.
Also, client application uses SignalR, which by default uses it's own transport. Hence, this and this are not options.
Why placing RCA an ICA certificates to "Trusted..." and "Intermediate..." folders doesn't help?
Is there any workaround?
The issue you are experiencing is because the subject CN presented by the certificate does not match the host name in the Uri.
Make sure that the certificate bound to the public IP address of the host does have a matching CN with the host name you are using to access the resource.
To easily verify, open the Url in a browser and view the certificate.
The Issued to field should contain a FQDN and match the host name part in the Uri. In your case, it does not.
Insert this piece of code on procedure body:
static void Main(string[] args)
{
ServicePointManager.ServerCertificateValidationCallback =
delegate (object sender, X509Certificate certificate, X509Chain
chain, SslPolicyErrors sslPolicyErrors)
{
return true;
};
....
}
The answer from #Qosai was unfortunately not enough for me, at least in a SignalR 3.1 client, as the websocket part also validates SSL certificates. ClientCertificateOptions needs to be set to Manual as well.
I found a post by a SignalR contributor that got me working:
_connection = new HubConnectionBuilder()
.WithUrl(new Uri(hub_uri), options => {
options
.Cookies
.Add(http_helper.loginCookie);
var handler = new HttpClientHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual,
ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true
};
options.HttpMessageHandlerFactory = _ => handler;
options.WebSocketConfiguration = sockets =>
{
sockets.RemoteCertificateValidationCallback = (sender, certificate, chain, policyErrors) => true;
};
})
.Build();
PS: If you still have issues, have a look at this article on how to enable logging properly. For my case it was a bit tricky because xUnit does not show console output. Therefore i enabled debugging logging to a file (not in the snippet)
I'm struggling to connect to a REST web service that's working only over HTTPS / SSL from my .NET application.
I received the certificate and private key to use as two separate files - a certificate.pem file which contains the certificate, and the webservice.key file which contains the private key. Those are both text files with BASE64 encoded binary data contained in them.
The provider also sent me a PDF showing how to call that web service using CURL and those two files, and that works just fine:
curl.exe -k -v "https://(URL)" --cert certificate.pem --key webservice.key
I need to use the -k option since there seems to be a self-signed certificate somewhere in the hierarchy of certs. Without this option, the call fails.
In order to call this web service from a .NET application (a console app for now), I used OpenSSL (on Windows) to combine these two files into a *.pfx file using this command:
openssl pkcs12 -export -out webservice.pfx -in certificate.pem -inkey webservice.key
This seems to have worked, too - no errors were reported, the file was created and is about 3K in size and it's a totally binary file.
Now, I tried to call that web service from my .NET code something like this:
try
{
// use the SSL protocol (instead of TLS)
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
// ignore any certificate complaints
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => { return true; };
// create HTTP web request with proper content type
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.ContentType = "application/xml;charset=UTF8";
// grab the PFX as a X.509 certificate from disk
string certFileName = Path.Combine(certPath, "webservice.pfx");
// load the X.509 certificate and add to the web request
X509Certificate cert = new X509Certificate(certFileName, "(top-secret password)");
request.ClientCertificates.Add(cert);
request.PreAuthenticate = true;
// call the web service and get response
WebResponse response = request.GetResponse();
Stream responseStream = response.GetResponseStream();
}
catch (Exception exc)
{
// log and print out error
}
However, I can try whatever I like (fiddling around with various settings, on the ServicePointManager and the HttpWebRequest, but I just keep getting these errors:
WebException: The underlying connection was closed: An unexpected error occurred on a send.
IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.
SocketException: An existing connection was forcibly closed by the remote host
and no response - even though communicating with the service with CURL has worked just fine.....
What am I missing?? I'm a bit puzzled and mystified by all those certificates, private keys, service point manager options and so on - just waaaaay too many knob and switches to turn, set or turn off - what are the RIGHT settings here??
Update:
If I use
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
then the error just simply is:
WebException: The request was aborted: Could not create SSL/TLS secure channel.
S O L U T I O N :
In the end, with looking at the output from curl and a lot of help from #Alexandru and #JurajMajer, I was able to get this to work with this code:
try
{
// use the TLS protocol
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
// create HTTP web request with proper content type
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.ContentType = "application/xml;charset=UTF8";
// grab the PFX as a X.509 certificate from disk
string certFileName = Path.Combine(certPath, "webservice.pfx");
// load the X.509 certificate and add to the web request
X509Certificate2 cert = new X509Certificate2(certFileName, "(top-secret password)");
request.ClientCertificates.Add(cert);
request.PreAuthenticate = true;
// call the web service and get response
WebResponse response = request.GetResponse();
Stream responseStream = response.GetResponseStream();
string xmlContents = new StreamReader(responseStream).ReadToEnd();
}
catch (Exception exc)
{
// log and print out error
}
You've used the X509Certificate(String, String) constructor with a PKCS#12 certificate, but that constructor only works for PKCS#7 certificates, as MSDN says it...
Initializes a new instance of the X509Certificate class using the name
of a PKCS7 signed file and a password to access the certificate.
PKCS#7 does not include the private (key) part of a certificate/private-key pair, which you will need. This means you will need to use your PKCS#12 certificate given the nature of your certificate.
You may want to try the X509Certificate2(String, String) constructor with your existing PKCS#12 certificate, as this constructor is used with PKCS#12 (PFX) files that contain the certificate's private key, as MSDN says...
This constructor creates a new X509Certificate2 object using a
certificate file name and a password needed to access the certificate.
It is used with PKCS12 (PFX) files that contain the certificate's
private key. Calling this constructor with the correct password
decrypts the private key and saves it to a key container.
Try to enable Network Tracing in App.config on the client - instructions here. That should create network.log with more debug info. In my test environment I have one pfx which works and one which doesn't.
network.log for working pfx:
SecureChannel#9343812 - We have user-provided certificates. The server has specified 34 issuer(s). Looking for certificates that match any of the issuers.
SecureChannel#9343812 - Left with 1 client certificates to choose from.
SecureChannel#9343812 - Trying to find a matching certificate in the certificate store.
SecureChannel#9343812 - Locating the private key for the certificate:
SecureChannel#9343812 - Certificate is of type X509Certificate2 and contains the private key.
network log for non-working pfx:
SecureChannel#26756241 - We have user-provided certificates. The server has specified 34 issuer(s). Looking for certificates that match any of the issuers.
SecureChannel#26756241 - Left with 0 client certificates to choose from.
So for me the problem is my non-working certificate was issued by CA not in list.
Interesting points (possible problems):
1.) Server sends the list of known issuers for client certificate.
2.) Client code is looking for certificate and private key in certificate store event though both are in pfx.
I am attempting to write a client-server library for letting my LAN computers talk to each other. As this is largely a self-education attempt, I am trying to make these connections occur over SSL. My plan is to maintain a single host machine, and have each client maintain a mutually authenticated System.Net.SslStream instance to the host. The host code looks (roughly speaking) like this (error handling omitted for brevity):
SslStream newStream = null;
newStream = new SslStream(this.client.GetStream(), false);
newStream.AuthenticateAsServer(
this.Certificate as X509Certificate,
true,
System.Security.Authentication.SslProtocols.Tls,
false);
if (!(
newStream.IsMutuallyAuthenticated &&
newStream.IsEncrypted &&
newStream.IsSigned))
{
throw new AuthenticationException("Authentication results unsatisfactory.");
}
In this, this.Certificate is an X509Certificate2. The client code looks like this:
SslStream connectionStream = new SslStream(this.client.GetStream(), false);
connectionStream.AuthenticateAsClient(
this.ServerHostname,
new X509CertificateCollection(new X509Certificate[] { (X509Certificate)this.Certificate }),
SslProtocols.Tls,
false);
if (!(
connectionStream.IsEncrypted &&
connectionStream.IsMutuallyAuthenticated &&
connectionStream.IsSigned))
{
throw new AuthenticationException("Authentication results unsatisfactory");
}
I have generated client and server certificates by following this post's instructions, and installed the root authority into Trusted Authorities for the local machine on both the client and server. I then generated certificates for both client and server, put copies of the .cer file in places accessible to the client/server (loaded to get the X509Certificate objects) and installed the with private keys in the Personal stores for the respective machines. Now, everything works when the client and server are on the same machine. But when I generate a new cert and put it on a second machine, the client and server now fail to mutually authenticate. The connection gets set up, encrypted and signed, but IsMutuallyAuthenticated is false. How can I fix this? What am I doing wrong?
Edit: LocalCertificateSelectionCallback and the other callback tell me that the client finds one local certificate, which is mine, no remote certificates, and no acceptable issuers. My certificate is selected and returned from the local certificate selection. There are no SSL policy errors on the client end, and on the host end, I still get RemoteCertificateNotAvailable. Sniffing this transaction with Wireshark, I see a standard TCP handshake, followed by a few hundred bytes of gibberish which includes the name of the host cert, and then the FIN, ACK packets that mark the end of the transaction. So the client certificate indeed isn't being sent, even though it's selected by the LocalCertificateSelectionCallback routine. What might cause this? How can I fix it?
There is a constructor of SSLStream which have RemoteCertificateVAlidationCallback parameter. It should help.
I have a fully operational system where openssl based clients interact with an openssl server. Each client have its own certificate that is validated by the server. Certificates have been generated with openssl (X509, pem). They are self-signed.
I now want to write a test client based on SslStream. I used the client example from the SslStream class documentation.
My SslStream client is unable to complete the handshake. stunnel complains about the client not sending its certificate. This is confirmed in Wireshark (Certificates Length: 0 in handshake messages).
My client displays the following exception:
Inner exception: The message received was unexpected or badly
formatted
This is how I load my certificate:
X509Certificate cert = new X509Certificate2(filename, password);
X509CertificateCollection certColl = new X509CertificateCollection();
certColl.Add(cert);
I tried retrieving certificate various properties (ex: GetSerialNumberString()). It works. The Verify method returns false. This is the next thing I am going to investigate.
How I setup my SslStream does not seem to matter (same result):
sslStream.AuthenticateAsClient(serverName);
SslStream sslStream = new SslStream(
client.GetStream(),
false,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
new LocalCertificateSelectionCallback(SelectLocalCertificate));
Same with authentication:
sslStream.AuthenticateAsClient(serverName);
sslStream.AuthenticateAsClient(serverName,
certColl,
SslProtocols.Tls,
true);
SelectLocalCertificate gets called (twice) and returns my certificate. ValidateServerCertificate currently never gets called (to my surprise).
How can I debug that? Even better if you can nail my problem.
UPDATE
I have added a function to perform chain validation based on the X509Chain example from the documentation. It displays all sorts of information on the certificate, including two intriguing messages:
Element certificate is valid: False
Element error status length: 1
In the end, I don't really have more details than when I call verify.
The output of openssl verify cert.pem does not report anything unusual.
error 18 at 0 depth lookup:self signed certificate
OK
UPDATE
I extracted the private key and certificate from the pem, and I generated a cert.pfx (pkcs12). I had no issues importing cert.pfx in my personal key store. In my certificate details, I can see a small icon indicating an attached private key.
I modified my client to retrieve the certificate from my personal store. But I am getting the same exact failure.
The solution was to import my CA root certificate on my Windows machine. My client is now able to complete the handshake!
Found the solution by searching for a more complete SslStream example. Thanks to http://geekswithblogs.net/luskan/archive/2007/10/01/115758.aspx.