I need to provide secure communication between various processes that are using TCP/IP sockets for communication. I want both authentication and encryption. Rather than re-invent the wheel I would really like to use SSL and the SslStream class and self-signed certificates. What I want to do is validate the remote process's certificate against a known copy in my local application. (There doesn't need to be a certificate authority because I intend for the certificates to be copied around manually).
To do this, I want the application to be able to automatically generate a new certifiate the first time it is run. In addition to makecert.exe, it looks like this link shows a way to automatically generate self-signed certificates, so that's a start.
I've looked at the AuthenticateAsServer and AuthenticateAsClient methods of SslStream. You can provide call-backs for verification, so it looks like it's possible. But now that I'm into the details of it, I really don't think it's possible to do this.
Am I going in the right direction? Is there a better alternative? Has anyone done anything like this before (basically peer-to-peer SSL rather than client-server)?
Step 1: Generating a self-signed certificate:
I downloaded the Certificate.cs class posted by Doug Cook
I used this code to generate a .pfx certificate file:
byte[] c = Certificate.CreateSelfSignCertificatePfx(
"CN=yourhostname.com", //host name
DateTime.Parse("2000-01-01"), //not valid before
DateTime.Parse("2010-01-01"), //not valid after
"mypassword"); //password to encrypt key file
using (BinaryWriter binWriter = new BinaryWriter(
File.Open(#"testcert.pfx", FileMode.Create)))
{
binWriter.Write(c);
}
Step 2: Loading the certificate
X509Certificate cert = new X509Certificate2(
#"testcert.pfx",
"mypassword");
Step 3: Putting it together
I based it on this very simple SslStream example
You will get a compile time error about the SslProtocolType enumeration. Just change that from SslProtocolType.Default to SslProtocols.Default
There were 3 warnings about deprecated functions. I replaced them all with the suggested replacements.
I replaced this line in the Server Program.cs file with the line from Step 2:
X509Certificate cert = getServerCert();
In the Client Program.cs file, make sure you set serverName = yourhostname.com (and that it matches the name in the certificate)
In the Client Program.cs, the CertificateValidationCallback function fails because sslPolicyErrors contains a RemoteCertificateChainErrors. If you dig a little deeper, this is because the issuing authority that signed the certificate is not a trusted root.
I don`t want to get into having the user import certificates into the root store, etc., so I made a special case for this, and I check that certificate.GetPublicKeyString() is equal to the public key that I have on file for that server. If it matches, I return True from that function. That seems to work.
Step 4: Client Authentication
Here's how my client authenticates (it's a little different than the server):
TcpClient client = new TcpClient();
client.Connect(hostName, port);
SslStream sslStream = new SslStream(client.GetStream(), false,
new RemoteCertificateValidationCallback(CertificateValidationCallback),
new LocalCertificateSelectionCallback(CertificateSelectionCallback));
bool authenticationPassed = true;
try
{
string serverName = System.Environment.MachineName;
X509Certificate cert = GetServerCert(SERVER_CERT_FILENAME, SERVER_CERT_PASSWORD);
X509CertificateCollection certs = new X509CertificateCollection();
certs.Add(cert);
sslStream.AuthenticateAsClient(
serverName,
certs,
SslProtocols.Default,
false); // check cert revokation
}
catch (AuthenticationException)
{
authenticationPassed = false;
}
if (authenticationPassed)
{
//do stuff
}
The CertificateValidationCallback is the same as in the server case, but note how AuthenticateAsClient takes a collection of certificates, not just one certificate. So, you have to add a LocalCertificateSelectionCallback, like this (in this case, I only have one client cert so I just return the first one in the collection):
static X509Certificate CertificateSelectionCallback(object sender,
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers)
{
return localCertificates[0];
}
you can look too this example
Sample Asynchronous SslStream Client/Server Implementation
http://blogs.msdn.com/joncole/archive/2007/06/13/sample-asynchronous-sslstream-client-server-implementation.aspx
if certificate is not produced correctly you can get exception The server mode SSL must use a certificate with the associated private key.
basic certificate example
makecert -sr LocalMachine -ss My -n CN=Test -sky exchange -sk 123456
or
as external file
makecert -sr LocalMachine -ss My -n CN=Test -sky exchange -sk 123456 c:\Test.cer
Certificate Creation Tool (Makecert.exe)
http://msdn.microsoft.com/en-us/library/bfsktky3%28VS.80%29.aspx
What you're proposing sounds fine to me, except that it sounds like you're looking to wait until the callback is invoked in order to generate the certificate. I don't think that that will fly; AFAIK, you've got to provide a valid certificate when you invoke AuthenticateAsX.
However, these classes are overridable; so in theory, you could create a derived class which first checks to see if a certificate needs to be generated, generates it if need be, then invokes the parent AuthenticateAsX method.
Related
I got a .p12 certificate file with 3 certificates in it. 2 of them are CA certificates.
If I use curl (7.70 on Win10) I can do:
curl -s -S -i --cert Swish_Merchant_TestCertificate_1234679304.p12:swish --cert-type p12 --tlsv1.2 --header "Content-Type:application/json" https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests --data-binary #jsondata.json
Curl will use the CA certificates in the p12 file when connecting to the server.
On the other hand, if I try to do something similar in .net core (3.1) it fails with the error message "The message received was unexpected or badly formatted."
var handler = new HttpClientHandler();
var certs = new X509Certificate2Collection();
certs.Import(#"Swish_Merchant_TestCertificate_1234679304.p12", "swish", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
foreach (var cert in certs)
{
handler.ClientCertificates.Add(cert);
}
var client = new HttpClient(handler);
var url = "https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests";
var request = new HttpRequestMessage()
{
RequestUri = new Uri(url),
Method = HttpMethod.Post,
};
request.Content = new StringContent(System.IO.File.ReadAllText(#"jsondata.json"), Encoding.UTF8, "application/json");
request.Headers.Add("accept", "*/*");
var response = await client.SendAsync(request);
Using Wireshark I saw that curl sends all three certificates from the p12 file whereas .net core only sends one. See images below.
If I install the CA certificates into "Personal certificate" for "Current User" then .net core also sends all three certificates and it works.
Question: Do I have to install the CA certificates (into the certificate store) when using .net core or is there a way to make it behave just like curl which uses the certificates from the p12 file?
Wireshark curl:
Wireshark .net core:
Short answer: no*.
Wordier intro: SslStream picks one certificate out of the ClientCertificates collection, using data that (was historically, but no longer generally) is sent by the TLS server about appropriate roots (and if none is applicable then it picks the first thing where HasPrivateKey is true). During the selection process each candidate certificate is checked in isolation, and it asks the system to resolve the chain. On Windows, the selected certificate is then sent down to the system libraries for "we're doing TLS now", which (IIRC) is where the limitations come from. (macOS and Linux builds of .NET Core just try to maintain behavioral parity)
Once the certificate is selected, there's one last chain-walk to determine what certificates to include in the handshake, it's done without the context of anything else from the ClientCertificates collection.
If you know that your collection represents one chain, your best answer is to import the CA elements into your user CertificateAuthority store. That store does not impart any trust to the CA certificates, it's really just a cache that's used when building chains.
Also, you don't want PersistKeySet, and probably don't want MachineKeySet: What is the rationale for all the different X509KeyStorageFlags?
var handler = new HttpClientHandler();
using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
var certs = new X509Certificate2Collection();
certs.Import(#"Swish_Merchant_TestCertificate_1234679304.p12", "swish", X509KeyStorageFlags.DefaultKeySet);
foreach (X509Certificate2 cert in certs)
{
if (cert.HasPrivateKey)
{
handler.ClientCertificates.Add(cert);
}
else
{
store.Add(cert);
}
}
}
var client = new HttpClient(handler);
...
* If your system already has the CA chain imported, it'll work. Alternatively, if the CA chain uses the Authority Information Access extension to publish a downloadable copy of the CA cert, the chain engine will find it, and everything will work.
I am creating a self-hosted owin server inside a windows service. I have implemented an ACME client to get a certificate from Let's Encrypt (to a domain given by the service's config.). However, if the service is run on a server which already has a certificate I do not need to request a new one. How can I determine, in code, if there is a certificate installed which applies for the domain set in the service's config?
The closest thing to a solution I found was to ignore existing certificates (if any) and always request a new one. Then when a certificate is received from Let's Encrypt, I save that certificate's serial to a file. On startup I then use the saved file (if any) to look for the existing certificate from the store:
public async Task<bool> NeedNewCertificate()
{
string certSerial = await AsyncFileHandler.ReadFileAsync(CERT_SERIAL_FILENAME);
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadOnly);
var certCollection = store.Certificates.Find(X509FindType.FindBySerialNumber, certSerial, true);
foreach (var cert in certCollection)
{
if (cert.NotBefore <= DateTime.Now && cert.NotAfter > DateTime.Now.AddDays(30)) // Let's Encrypt certificates are valid 90 days. They recommend renewing certificates every 30 days.
return false;
}
}
return true;
}
That's a good enough way of doing it I think.
Alternatively you can do a search in the certificate store that matches the parameters that you specify: the domain that the certificate is applied to, expiry date, etc. If you find the one that is valid, then you can use it.
That is in fact most likely the definition for what you mean by "certificate is installed on the server".
The attribute you probably would like to check is "Subject", e.g. you wold like these that have "CN = mydomain.com". You can have wildcard certificates as well, so you will need to figure out what types of certificates can be installed.
I create a certificate request like this:
certreq -new req.inf req-Revoked.req
certreq -submit -attrib "SAN:email=ttesting#Test.Domain&upn=1234567890#Test.Domain" -config Win2K8-64\Test-Win2K8-64-CA req-Revoked.req testerCert-Revoked.cer
certreq -accept testerCert-Revoked.cer
CertUtil -f -p testerCert -exportPFX -user My Testing.Tester.T.1234567890 testerCert-Revoked.pfx
CertUtil -delstore -user My Testing.Tester.T.1234567890
Then I revoke it, via:
CertUtil -revoke <SerialNumber_from_above_Cert>
I then execute this code:
X509Certificate2 certificate = GetCertificate("testerCert-Revoked.pfx", "password"); // helper method loads the pfx from a file
X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.VerificationTime = DateTime.Now;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
if (!chain.Build(certificate))
{
errorBuffer.Append("Could not build X.509 certificate chain:\r\n");
foreach (X509ChainStatus status in chain.ChainStatus)
{
errorBuffer.AppendFormat(" - {0}\r\n", status.StatusInformation);
}
throw new CryptographicException(errorBuffer.ToString());
}
chain.Build() always returns true. But since the certificate is revoked, it should be False! I've double-checked that the certificate is revoked, and that the serial number is listed in the Server Manager under revoked certificates. I've double-checked that the CURL distribution point urls match on the server and the certificate requests. (they're LDAP urls). CertUtil sees the intermediate .cer file as revoked. But the C# code above doesn't.
This all worked before the original CA expired, and IT rebuilt my test machine with a new cert. I've regenerated the certs with the new CA, and every single unit tests works again, except the ones that deal with revocation. Expired certs work as expected, but not revoked certs.
I'm at a loss for what to do next to get this code working again, and would love some help. Thanks!
Turns out the problem was an application called Tumbleweed. It was installed on the server, and was intercepting all revocation requests. Disabling the Tumbleweed service fixed my problem entirely.
Charles.
The chain is simply a list of the certificates, starting with the one you providing, all the way up to the root certificate. It's built out of whatever certificates can be found -- even if they are out of date, invalid, or revoked.
What you're trying to do is validate the chain. Use the chain.ChainStatus for that.
From the MSDN docs:
The X509Chain object has a global error status called ChainStatus that should be used for certificate validation. The rules governing certificate validation are complex, and it is easy to oversimplify the validation logic by ignoring the error status of one or more of the elements involved. The global error status takes into consideration the status of each element in the chain.
I am working on a build a tool to test all my SSL certificates in my environment.
I am using the standard SSLStream implementation to connect to the remote servers, authenticate as a client and then use the ssl.RemoteCertificate method to retrieve the Cert.
The code works fine, I get the cert. I build a chain from the cert, loop through the chain and enumerate all the certs in the chain.
I thought that I had gotten it working until I compared my output to DigiCerts SSL Utility.
I noticed that the chains that I got from it are different than the chain that I built through C#.
Digging a little more, I realized that, in my local store where the C# code is executed, there are multiple intermediate certs with the same subject name.
The only difference is there expiry.....
it appears that C# takes the newest cert with the oldest expiry....
4/29/2017
Where as the DigiCert tool displays the intermediate cert that is older with the expiry that is closer to expiring IE 4/29/2014
Is there a way to control how the chain is built? (inside of C#)
using (TcpClient client = new TcpClient())
{
try
{
client.Connect("servername", 443);
SslStream ssl = new SslStream(client.GetStream(), false, new RemoteCertificatValidationCallback(ValidateServerCertificate), null);
ssl.AuthenticateAsClient("servername");
cert = new X509Certificate2(ssl.RemoteCertificate);
PrintChain(cert);
}
catch(Exception ex)......
}
private static void PrintChain(X509Certificate cert)
{
X509Chain ch = new X509Chain();
ch.Build(cert);
ch.ChainPolicy.RevocationMode = X509RevocationMode.Online;
foreach (X509ChainElement element in ch.ChainElements)
{
Console.WriteLine(element.Certificate.SerialNumber);
// Go thru and print all my details and continue the loop
}
I have also overridden the ValidateServerCertificate method and used the chain directly from that override instead of using the X509Cahin.Build();
they print out the same.....
Thank you
I'm trying to authenticate myself against WebService using my client certificate, but, for some reasons (I explain), I don't want to load certificate from store, rather read it from disc.
The following:
// gw is teh WebService client
X509Certificate cert = new X509Certificate(PathToCertificate);
_gw.ClientCertificates.Add(ClientCertificate());
ServicePointManager.ServerCertificateValidationCallback = (a,b,c,d) => true;
_gw.DoSomeCall();
returns always 403 - the Service doesn't authorize me. But, when I save that certificate into CertStore, it works. (As stated in MSDN.)
Is it possible to use certificate not in store?
(the reason is, that I got windows service(client) sometimes calling webservice(server), and after unspecified amount of time the service 'forgets' my certificates and doesnt authorize against server, with no apparent reason)
What type of file is PathToCertificate? If it's just a .cer file, it will not contain the private key for the certificate and trying to use that certificate for SSL/TLS will fail.
However, if you have a PKCS7 or PKCS12 file that includes the public and private key for the certificate, your code will work (you might need to use the overload that takes a password if the private key has one).
To test this, I went to http://www.mono-project.com/UsingClientCertificatesWithXSP and created my client.p12 file following those instructions. I also created a simple HTTPS server using HttpListener for testing.
Then I compiled the following program into 'client.exe' and run like:
client.exe https://<MYSSLSERVER>/ client.p12 password
where client.p12 is the PKCS12 file generated before and 'password' is the password I set for the private key of the certificate.
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
public class HttpWebRequestClientCertificateTest : ICertificatePolicy {
public bool CheckValidationResult (ServicePoint sp, X509Certificate certificate,
WebRequest request, int error)
{
return true; // server certificate's CA is not known to windows.
}
static void Main (string[] args)
{
string host = "https://localhost:1234/";
if (args.Length > 0)
host = args[0];
X509Certificate2 certificate = null;
if (args.Length > 1) {
string password = null;
if (args.Length > 2)
password = args [2];
certificate = new X509Certificate2 (args[1], password);
}
ServicePointManager.CertificatePolicy = new HttpWebRequestClientCertificateTest ();
HttpWebRequest req = (HttpWebRequest) WebRequest.Create (host);
if (certificate != null)
req.ClientCertificates.Add (certificate);
WebResponse resp = req.GetResponse ();
Stream stream = resp.GetResponseStream ();
StreamReader sr = new StreamReader (stream, Encoding.UTF8);
Console.WriteLine (sr.ReadToEnd ());
}
}
Let me know if you want me to upload the server code and the certificates used on both sides of the test.
The potential problem could be caching of SSL sessions (Schannel cache). Only first request negotiates the SSL handshake. Subsequent requests will use the same session ID and hope that the server accept it. If the server clears the SessionId, the requests will fail with 403 error. To disable local ssl session caching (and force SSL negotiation for each request) you have to open windows registry folder:
[HKEY_LOCAL_MACHINE][System][CurrentControlSet][Control][SecurityProviders][SCHANNEL]
and add the key named ClientCacheTime (DWORD) with value 0.
This issue is covered here:
http://support.microsoft.com/?id=247658
You have the potential for at least two problems...
First...
Your client certificate file cannot contain a private key unless it's accessed with a password. You should be using a PKCS #12 (*.pfx) certificate with a password so that your client has access to the private key. You client code will have to provide the password when opening the certificate as others have already posted. There are several ways to create this, the easiest is to use the following command-line to first generate the certificate, then use the MMC certificate manager to export the certificates private key:
Process p = Process.Start(
"makecert.exe",
String.Join(" ", new string[] {
"-r",// Create a self signed certificate
"-pe",// Mark generated private key as exportable
"-n", "CN=" + myHostName,// Certificate subject X500 name (eg: CN=Fred Dews)
"-b", "01/01/2000",// Start of the validity period; default to now.
"-e", "01/01/2036",// End of validity period; defaults to 2039
"-eku",// Comma separated enhanced key usage OIDs
"1.3.6.1.5.5.7.3.1," +// Server Authentication (1.3.6.1.5.5.7.3.1)
"1.3.6.1.5.5.7.3.2", // Client Authentication (1.3.6.1.5.5.7.3.2)
"-ss", "my",// Subject's certificate store name that stores the output certificate
"-sr", "LocalMachine",// Subject's certificate store location.
"-sky", "exchange",// Subject key type <signature|exchange|<integer>>.
"-sp",// Subject's CryptoAPI provider's name
"Microsoft RSA SChannel Cryptographic Provider",
"-sy", "12",// Subject's CryptoAPI provider's type
myHostName + ".cer"// [outputCertificateFile]
})
);
Second...
Your next problem is going to be server-side. The server has to allow this certificate. You have the right logic, but on the wrong side of the wire, move this line to the web server handling the request. If you cannot, you must then take the '.cer' file saved above to the server and add it to the server computer's trust list:
ServicePointManager.ServerCertificateValidationCallback = (a,b,c,d) => true;
Do you need a password for the certificate? If so, there is a field for it in the constructor.
X509Certificate cert = new X509Certificate(PathToCertificate,YourPassword);