I have a fine, working communicator application, written in C#. Now I need to implement a secure connection to the server. I've tried changing Socket and TcpClient objects into SslStream, but I got a few errors.
Firstly I generated a .cer certificate using makecert. OpenSSL certificates didn't worked for me, because they didn't include private key. Next I created a certificate object on the server and bound it to the SslStream:
X509Certificate serverCertificate = X509Certificate.CreateFromCertFile("ca.cer");
TcpClient clientSocket = this.tcpListener.AcceptTcpClient();
SslStream ssls = new SslStream(clientSocket.GetStream(), false);
ssls.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true);
On the client side I'm doing this:
TcpClient client = new TcpClient(settings.IP, settings.Port);
sslStream = new SslStream(client.GetStream(), false,
new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
sslStream.AuthenticateAsClient(settings.IP);
public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
Console.WriteLine("Certificate error: {0}", sslPolicyErrors);
return false;
}
but the AuthenticateAsClient function throws an exception. I tried several other method parameters, but none of this worked for me.
Could anyone help me, or explain step by step how to change simple sockets into secure ones? To be clear, SSL isn't the requirement - if there is simpler way to make the connection secure I can use that.
Edit:
Message in exception is following:
The remote certificate is invalid according to the validation procedure.
What's the exception ?
I guess you are getting the exception because your client certificate is rejected as valid from the server (unless you did the necessary things to set up trust).
When you use self signed certificates(the ones created by makecert), you would have to also supply a Client Certificate Validation callback when creating the SslStream object. Use this constructor overload http://msdn.microsoft.com/en-us/library/ms145056.aspx
.CER file usually contains a certificate without a private key. Loading it into the server just won't work. You need a private key as well. The easiest is to generate the certificate and a key and save them together to PFX format. Then load the certificate and a key from PFX on the server.
Now with your code only only authenticate the server. Is this what you want to do (i.e. don't you want to authenticate the client on the server as well)?
Related
I was reading MSDN info articles for quite a long time and still I fail to understand it.
Based on the assumption that client Authentication is not required:
1.When I call SslStream.AuthenticateAsServer(...) do I call this method on the server side or on the client side?
2.When establishing a SslStream is it only the responsibility of the server to establish the SslStream or both server and client?
3.If it is only the responsibility of the server, does it mean that the client can just use regular send() and receive() operations without creating a SslStream by himself?
4.Does the client need to get the certificate file in order to authenticate the server?
Thank you very much in advance, I really could not find much information about this topic and I've been searching for this information for a long time...
EDIT: the MSDN has a complete working example at the bottom of this page: https://msdn.microsoft.com/en-us/library/system.net.security.sslstream?f=255&MSPPError=-2147217396 - so you should really start experimenting there because that example has it all.
Original answer:
I must preface this answer that "client authentication not required" is the case for most of the SSL implementations. Client authentication is rare: you're likely to see it in VPN apps, the banking industry and other secure apps. So it would be wise when you are experimenting with SslStream() to start without client authentication.
When you browse to an HTTPS website, you don't authenticate your browser with a client cert, instead you just want to confirm the server name you are connecting to matches up to the CNAME found in the cert and that the server cert is signed by a CA that your machine trusts - there's more to it, but essentially that's what it boils down to.
So, having said that, let me answer your questions:
1) SslStream.AuthenticateAsServer(...) is done ONLY on server side with the server 509 certificate. On the client side, you must call SslStream.AuthenticateAsClient(serverName) with server name being the CNAME (common name) of your certificate (example: "domain.com")
2) SslStream must be created for both the client and the server. You create it simply by "wrapping" a TcpClient NetworkStream around it (for example, but there are other methods)
Example for the server:
// assuming an 509 certificate has been loaded before in an init method of some sort
X509Certificate serverCertificate = X509Certificate2.CreateFromCertFile("c:\\mycert.cer"); // for illustration only, don't do it like this in production
...
// assuming a TcpClient tcpClient was accepted somewhere above this code
slStream sslStream = new SslStream(tcpClient.GetStream(), false);
sslStream.AuthenticateAsServer(
serverCertificate,
false,
SslProtocols.Tls,
true);
3) No. The communication is encrypted on both ends. So both sides must use SslStream. Using receive() and send() on the client would yield binary encrypted data.
4) No. The client passes a callback method to the SslStream creation in order to validate the certificate received by the server.
Example:
// assuming a TcpClient tcpClient was connected to the server somewhere above this code
SslStream sslStream = new SslStream(
tcpClient.GetStream(),
false,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
null
);
sslStream.AuthenticateAsClient(serverName); // serverName: "domain.com" for example
then somewhere else in your code:
public static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None) {
return true;
}
Console.WriteLine("Certificate error: {0}", sslPolicyErrors);
// refuse connection
return false;
}
The RemoteCertificateValidationCallback Delegate, shown below, is used to verify the remote Secure Sockets Layer (SSL) certificate. The certificate parameter is the end entity server certificate returned by the remote server. What I'm not sure about is how the chain parameter is constructed. Is it built from the the list of certificates returned by the remote server (typically the server cert and intermediate CA certs) or does it go to the certificate local store and try to build a chain for the end entity server certificate returned in the certificate parameter?
// The following method is invoked by the RemoteCertificateValidationDelegate.
public static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
...
}
UPDATE
I've got a suspicion it's using X509Chain.Build(server_cert), but would like to know what's really happening.
This person sees something similar: c# Retrieving a Certificate from an SSL stream shows different chain results vs other external tools
a-certificate-from-an-ssl-stream-shows-different-chain-result
It is received from the server as part of the TLS handshake:
7.4.2. Server Certificate
When this message will be sent:
The server MUST send a Certificate message whenever the agreed-
upon key exchange method uses certificates for authentication
(this includes all key exchange methods defined in this document
except DH_anon). This message will always immediately follow the
ServerHello message.
Meaning of this message:
This message conveys the server's certificate chain to the client.
The certificate MUST be appropriate for the negotiated cipher
suite's key exchange algorithm and any negotiated extensions.
Structure of this message:
opaque ASN.1Cert<1..2^24-1>;
struct {
ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;
certificate_list
This is a sequence (chain) of certificates. The sender's
certificate MUST come first in the list. Each following
certificate MUST directly certify the one preceding it. Because
certificate validation requires that root keys be distributed
independently, the self-signed certificate that specifies the root
certificate authority MAY be omitted from the chain, under the
assumption that the remote end must already possess it in order to
validate it in any case.
Well, according to X509Chain.BuildChain() it uses CAPI CertGetCertificateChain, which means is taken from local cert store, built up from a given cert. You can see how the validation callback is invoked in TransportSecurityHelpers.cs. The chain is built in _SecureChannel.VerifyRemoteCertificate:
chain = new X509Chain();
chain.ChainPolicy.RevocationMode = m_CheckCertRevocation? X509RevocationMode.Online : X509RevocationMode.NoCheck;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
if (remoteCertificateStore != null)
chain.ChainPolicy.ExtraStore.AddRange(remoteCertificateStore);
if (!chain.Build(remoteCertificateEx) // Build failed on handle or on policy
&& chain.ChainContext == IntPtr.Zero) // Build failed to generate a valid handle
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
So it looks like the .Net takes the server cert from the SSL context and builds the chain using the CAPI chain building functions.
Using .NET 4.0 framework I'm trying to make a server to server HTTPS SOAP service call.
On the client side server, I'm trying to add a local SSL certificate so that the server receiving the call could identify me.
The certificate I'm trying to add is not from the local machine's store (the problem persists even if it is), just a (.cer) file placed as a local file on my machine.
The code works fine and seems to add the certificate, but my System.Net.trace.log file keeps adding the following error for every call:
"Cannot find the certificate in either the LocalMachine store or the CurrentUser store." and the server I'm trying to call keeps telling me there's no certificate attached to my request.
The final WebException I get is:
"The request was aborted: Could not create SSL/TLS secure channel."
My GetCertificate function:
private X509Certificate2 GetCertificate()
{
X509Certificate2 cert;
try
{
cert = new X509Certificate2("MyCertificateFoder\\MyCertificate.cer");
}
catch (Exception ex)
{
throw;
}
return cert;
}
Service call code:
X509Certificate2 cert = GetCertificate();
ServiceClient.ClientCertificates.Add(cert);
serviceMethodResponse = ServiceClient.ServiceMethod(serviceMethodRequest);
Will appreciate any advice.
Thanks
The principal mistake here is that when using SSL with client authentication, a .pfx file (with client private key, certificates, certificate chains, root authority certificates) should be used.
NOT the .cer file.
This is the cause of the error.
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.