I have an Azure cloud service that uses a self-signed certificate to secure an SslStream to an application I have running locally. Since neither of these applications should ever be exposed to the outside world and for other reasons, I'm not interested in "properly" loading the certificate from a machine store; I just want an encrypted stream and a simple means of authentication.
The following code works as intended (the SslStream authenticates and encrypts) when the certificate is loaded from an embedded project resource, but it fails on the call to AuthenticateAsServer() when the certificate is loaded from bytes in a database. I have verified that the certificates are identical (byte by byte), regardless of whether they're loaded from the embedded file or from SQL, so I don't understand why when I use SQL I get an exception ("the server mode ssl must use a certificate with the associated private key"):
NetStream = new SslStream(Client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateClientCertificate));
NetStream.AuthenticateAsServer(cert, true, System.Security.Authentication.SslProtocols.Default, false);
bool ValidateClientCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
try
{
byte[] clientCertBytes = certificate.GetRawCertData();
byte[] serverCertBytes = _serverCert.GetRawCertData();
if(clientCertBytes.Length != serverCertBytes.Length)
{
throw new Exception("Client/server certificates do not match");
}
for(int i = 0; i < clientCertBytes.Length; i++)
{
if(clientCertBytes[i] != serverCertBytes[i])
{
throw new Exception("Client/server certificates do not match");
}
}
}
catch(Exception ex)
{
SystemLogger.LogException(ex, "SslServerClient");
return false;
}
return true;
}
This is the code that loads the cert from an embedded resource:
Assembly assembly = Assembly.GetExecutingAssembly();
using(Stream stream = assembly.GetManifestResourceStream("Resources.localhost.pfx"))
{
using(MemoryStream memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
_serverCert = new X509Certificate2(memoryStream.ToArray(), "password");
}
}
This is the code that loads the cert from SQL:
private static X509Certificate2 readCertificateFromSql(SqlParameterHelper sph)
{
X509Certificate2 result = null;
byte[] certBytes;
string certPassword;
using(IDataReader reader = sph.ExecuteReader())
{
if(reader.Read())
{
try
{
certBytes = (byte[])reader["Data"];
certPassword = (string)reader["Password"];
result = new X509Certificate2(certBytes, certPassword);
}
catch(Exception ex)
{
throw ex;
}
}
}
return result;
}
EDIT: I've found that the difference between the two X509Certificate2 objects seems to be that the one loaded from SQL has a PrivateKey property of NULL. I don't understand why, since a call to GetRawCertData() from each certificate returns exactly the same byte array.