Root Certificate Pinning in C#/.NET - c#

I want to implement certificate/public key pinning in my C# application. I already saw a lot of solutions that pin the certificate of the server directly as e.g. in this question. However, to be more flexible I want to pin the root certificate only. The certificate the server gets in the setup is signed by an intermediate CA which itself is signed by the root.
What I implemented so far is a server that loads its own certificate, the private key, intermediate certificate, and the root certificate from an PKCS#12 (.pfx) file. I created the file using the following command:
openssl pkcs12 -export -inkey privkey.pem -in server_cert.pem -certfile chain.pem -out outfile.pfx
The chain.pem file contains the root and intermediate certificate.
The server loads this certificate and wants to authenticate itself against the client:
// certPath is the path to the .pfx file created before
var cert = new X509Certificate2(certPath, certPass)
var clientSocket = Socket.Accept();
var sslStream = new SslStream(
new NetworkStream(clientSocket),
false
);
try {
sslStream.AuthenticateAsServer(cert, false, SslProtocols.Tls12, false);
} catch(Exception) {
// Error during authentication
}
Now, the client wants to authenticate the server:
public void Connect() {
var con = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
con.Connect(new IPEndPoint(this.address, this.port));
var sslStream = new SslStream(
new NetworkStream(con),
false,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
null
);
sslStream.AuthenticateAsClient("serverCN");
}
public static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors
)
{
// ??
}
The problem now is that the server only sends its own certificate to the client. Also the chain parameter does not contain further information.
This is somehow plausible as the X509Certificate2 cert (in the server code) only contains the server certificate and no information about the intermediate or root certificate. However, the client is not able to validate the whole chain as (at least) the intermediate certificate is missing.
So far, I did not find any possiblity to make .NET send the whole certificate chain, but I do not want to pin the server certificate iself or the intermediate one as this destroys the flexibility of root certificate pinning.
Therefore, does anyone know a possibility to make SslStream sending the whole chain for authentication or implement the functionality using another approach? Or do I have to pack the certificates differently?
Thanks!
Edit:
I made some other tests to detect the problem. As suggested in the comments, I created a X509Store that contains all the certificates. After that, I built a X509Chain using my server's certificate and the store. On the server itself, the new chain contains all the certificates correctly, but not in the ValidateServerCertificate function..

SslStream will never send the whole chain (except for self-issued certificates). The convention is to send everything except for the root, because the other side either already has and trusts the root or doesn't have (thus/or doesn't trust the root), and either way it was a waste of bandwidth.
But SslStream can only send the intermediates when it understands the intermediates.
var cert = new X509Certificate2(certPath, certPass);
This only extracts the end-entity certificate (the one with the private key), it discards any other certificates in the PFX. If you want to load all of the certificates you need to use X509Certificate2Collection.Import. But... that doesn't really help you either. SslStream only accepts the end-entity certificate, it expects the system to be able to build a functioning chain for it.
In order to build a functioning chain, your intermediate and root certificates need to be in any of:
Provided as manual input via X509Chain.ChainPolicy.ExtraStore
Since the chain in question is built by SslStream you can't really do this here.
CurrentUser\My X509Store
*LocalMachine\My X509Store
CurrentUser\CA X509Store
**LocalMachine\CA X509Store
CurrentUser\Root X509Store
**LocalMachine\Root X509Store
*LocalMachine\ThirdPartyRoot X509Store
An http (not-s) location identified in an Authority Access Identifier extension in the certificate.
The stores marked with * don't exist on .NET Core on Linux. The stores marked with ** do exist on Linux, but cannot be modified by a .NET application.
That's also not quite sufficient, because (at least for SslStream on Linux and probably macOS on .NET Core) it still only sends the intermediates if it built a chain it trusted. So the server needs to actually trust the root certificate for it to send the intermediates. (Or the client needs to trust the root for the client cert)
On the other side, the same rules apply. The difference is that in the callback you can choose to rebuild the chain to add the extra certificates.
private static bool IsExpectedRootPin(X509Chain chain)
{
X509Certificate2 lastCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
return lastCert.RawBytes.SequenceEquals(s_pinnedRootBytes);
}
private static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors
)
{
if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)
{
// No cert, or name mismatch (or any future errors)
return false;
}
if (IsExpectedRootPin(chain))
{
return true;
}
chain.ChainPolicy.ExtraStore.Add(s_intermediateCert);
chain.ChainPolicy.ExtraStore.Add(s_pinnedRoot);
chain.ChainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority;
if (chain.Build(chain.ChainElements[0].Certificate))
{
return IsExpectedRootPin(chain);
}
return false;
}
Of course, the problem with this approach is that you need to also understand and provide the intermediate on the remote side. The real solution to this is that the intermediates should be available on an HTTP distribution endpoint and the issued certificates should carry the Authority Information Access extension to be able to locate them dynamically.

Related

Unity TlsException: Handshake failed UNITYTLS_X509VERIFY_FLAG_NOT_TRUSTED

I'm trying to update my application's TcpClient to use TLS with SslStream instead of the normal Stream, the code i'm using for this seems to work outside of Unity, but fails when integrated in my Unity 2019.1.8 (tested on 2018 and 2017 as well) project.
To establish a connection and open a new SslStream I use the following code:
public static void InitClient(string hostName, int port, string certificateName)
{
client = new TcpClient(hostName, port);
if (client.Client.Connected)
{
Debug.LogFormat("Client connected succesfully");
}
else
{
Debug.LogErrorFormat("Client couldn't connect");
return;
}
stream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
try
{
stream.AuthenticateAsClient(certificateName);
}
catch (AuthenticationException e)
{
Debug.LogErrorFormat("Error authenticating: {0}", e);
if (e.InnerException != null)
{
Debug.LogErrorFormat("Inner exception: {0}", e);
}
Debug.LogErrorFormat("Authentication failed - closing connection");
stream.Close();
client.Close();
}
}
And for validating the certificate
public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
Debug.LogErrorFormat("Certificate error: {0}", sslPolicyErrors);
return false;
}
In Unity 2019.1.8 the client connects and will attempt to validate the remote certificate, which fails with the error TlsException: Handshake failed - error code: UNITYTLS_INTERNAL_ERROR, verify result: UNITYTLS_X509VERIFY_FLAG_NOT_TRUSTED.
Making ValidateServerCertificate always return true lets my client connect without issue.
I tried replicating the issue in a standalone C# Console Application targeting .net framework 4.7.1 using the exact same code. Launching the client in this application will return true from ValidateServerCertificate from the sslPolicyErrors == SslPolicyErrors.None check.
I know that the certificate is a valid cert, issued by a trusted CA (as verified by the fact that the cert is accepted from a console app, and it having a padlock in browsers).
Why does the validation fail in Unity, but nowhere else?
Why Unity can't validate option 1:
Although the certificate is valid and correct (e.g. it works when using it in a web browser), it did not include the intermediate certificates chain to the root CA. This has as a result that no chain of trust can be formed (Unity doesn't cache/retrieve intermediates), resulting in the UNITYTLS_X509VERIFY_FLAG_NOT_TRUSTED flag being set.
To fix this I needed to append the certificate chain to my leaf certificate so that Unity can verify the entire chain up to the root CA. To find the certificate chain for your certificate you can use a "TLS certificate chain composer".
Depending on what software you use you may either need to include the chain in your certificate, or keep it in a seperate file. From whatsmychaincert (i'm in no way affiliated to this site, merely used it):
Note: some software requires you to put your site's certificate (e.g. example.com.crt) and your chain certificates (e.g. example.com.chain.crt) in separate files, while other software requires you to put your chain certificates after your site's certificate in the same file.
Why Unity can't validate option 2:
When your server's SSL certificate has expired Unity will throw the exact same UNITYTLS_X509VERIFY_FLAG_NOT_TRUSTED error without additional information that the certificate has been expired, so make sure the "Valid to" date is in the future (this would also cause the certificate to be denied when used in a web browser).
Why a browser/Console app can validate:
Software can have different implementation on how it deals with incomplete chains. It can either throw an error, stating that the chain is broken and can thus not be trusted (as is the case with Unity), or cache and save the intermediates for later use/retrieve it from previous sessions (as browsers and Microsoft's .net(core) do).
As explained in this answer (emphasis mine)
In general, SSL/TLS clients will try to validate the server certificate chain as received from the server. If that chain does not please the client, then the client's behaviour depends on the implementation: some clients simply give up; others (especially Windows/Internet Explorer) will try to build another chain using locally known intermediate CA and also downloading certificates from URL found in other certificates (the "authority information access" extension).
Not trusting the certificate when no chain is provided is a deliberate decision made by Unity due to cross-platform compatability, as stated in an [answer to issue 1115214][3]:
We may be able to solve this issue by doing a verification via the system specific TLS api instead of using OpenSSL/MbedTLS to validate against root certificates as we do today, however this solution would then not work cross-platform. So we don't want to implement it today, as it would hide the misconfigured server from the user on some but not all platforms.
I figured out this was a solution to my particular situation while asking this question, so decided to self-answer it for future references. However UNITYTLS_X509VERIFY_FLAG_NOT_TRUSTED can have all kind of causes, this just being one of them.
For anyone stumbling across this who's using UnityWebRequest and Let's Encrypt:
There is a known issue on Unity's Issue Tracker where you will find it's been fixed in only certain versions:
Fixed in 2020.1.0a11
Fixed in 2019.3.3f1
Fixed in 2018.4.18f1
Changing Unity versions for an existing project is not a trivial task so we opted to buy a legit SSL cert which, although not cheap, was the simplest solution.
My solution was:
> Public class ForceAcceptAll : CertificateHandler {
> protected override bool ValidateCertificate(byte[] certificateData)
> {
> return true;
> } }
//-------
var cert = new ForceAcceptAll();
// www is a UnityWebRequest
www.certificateHandler = cert;
Nota: if used incorrectly (and found by the Google, your build might get rejected).
So use it with caution.
more information here.

How to Connect to a Web-service that has a faulty security certificate [duplicate]

We are setting up a new SharePoint for which we don't have a valid SSL certificate yet. I would like to call the Lists web service on it to retrieve some meta data about the setup. However, when I try to do this, I get the exception:
The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.
The nested exception contains the error message:
The remote certificate is invalid according to the validation procedure.
This is correct since we are using a temporary certificate.
My question is: how can I tell the .Net web service client (SoapHttpClientProtocol) to ignore these errors?
Alternatively you can register a call back delegate which ignores the certification error:
...
ServicePointManager.ServerCertificateValidationCallback = MyCertHandler;
...
static bool MyCertHandler(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors error)
{
// Ignore errors
return true;
}
Like Jason S's answer:
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
I put this in my Main and look to my app.config and test if (ConfigurationManager.AppSettings["IgnoreSSLCertificates"] == "True") before calling that line of code.
I solved it this way:
Call the following just before calling your ssl webservice that cause that error:
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
/// <summary>
/// solution for exception
/// System.Net.WebException:
/// 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.
/// </summary>
public static void BypassCertificateError()
{
ServicePointManager.ServerCertificateValidationCallback +=
delegate(
Object sender1,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
return true;
};
}
The approach I used when faced with this problem was to add the signer of the temporary certificate to the trusted authorities list on the computer in question.
I normally do testing with certificates created with CACERT, and adding them to my trusted authorities list worked swimmingly.
Doing it this way means you don't have to add any custom code to your application and it properly simulates what will happen when your application is deployed. As such, I think this is a superior solution to turning off the check programmatically.
I was having same error using DownloadString; and was able to make it works as below with suggestions on this page
System.Net.WebClient client = new System.Net.WebClient();
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
string sHttpResonse = client.DownloadString(sUrl);
ServicePointManager.ServerCertificateValidationCallback +=
(mender, certificate, chain, sslPolicyErrors) => true;
will bypass invaild ssl . Write it to your web service constructor.
For newbies,
you can extend your partial service class in a separate cs file and add the code the code provided by "imanabidi" to get it integrated
To further expand on Simon Johnsons post - Ideally you want a solution that will simulate the conditions you will see in production and modifying your code won't do that and could be dangerous if you forget to take the code out before you deploy it.
You will need a self-signed certificate of some sort. If you're using IIS Express you will have one of these already, you'll just have to find it. Open Firefox or whatever browser you like and go to your dev website. You should be able to view the certificate information from the URL bar and depending on your browser you should be able to export the certificate to a file.
Next, open MMC.exe, and add the Certificate snap-in. Import your certificate file into the Trusted Root Certificate Authorities store and that's all you should need. It's important to make sure it goes into that store and not some other store like 'Personal'. If you're unfamiliar with MMC or certificates, there are numerous websites with information how to do this.
Now, your computer as a whole will implicitly trust any certificates that it has generated itself and you won't need to add code to handle this specially. When you move to production it will continue to work provided you have a proper valid certificate installed there. Don't do this on a production server - that would be bad and it won't work for any other clients other than those on the server itself.

Getting SmtpClient to work with a self signed SSL certificate

I'm attempting to use the System.Net.Mail.SmtpClient class to relay an email through my company's email server. All SMTP connections to the mail server have to be SSL and it uses a self signed certificate. That's fine for Outlook where you can just click ok on the warning dialogue but does anyone know a way to get SmtpClient to accept a self signed certificate?
I'm planning on using this app on the Windows Azure Platform so I won't be able to install the self signed certificate as a trusted root.
You may take a look at the ServerCertificateValidationCallback property:
ServicePointManager.ServerCertificateValidationCallback =
(sender, certificate, chain, sslPolicyErrors) => true;
It represents a callback which is called by the runtime when it tries to validate an SSL certificate. By returning true you basically say that you don't care if the certificate is valid or not -> you always accept it. Of course having self signed certificates in production environment is not a good idea.
My issue ended up being that the .Net SmtpClient class apparently doesn't support the use of port 465 for SMTP SSL connections. Using port 25 with a self signed SSL certificate worked correctly.
MSDN System.Net forum question Can SmtpClient be configured to work with a self signed certificate?.
If you want to be more secure, you might want to look at doing the following:
theClient.EnableSsl = true;
ServicePointManager.ServerCertificateValidationCallback =
(sender, certificate, chain, sslPolicyErrors) => {
if (sender == theClient) {
return true;
} else {
// you should apply the code from this SO answer
// https://stackoverflow.com/a/25895486/795690
// if you find that anything in your app uses this path
throw new ApplicationException("Certificate validation is currently disabled, extra code neeeded here!");
}
};
In this code we are auto-approving certificates only for the specific SMTP client in question; we have a stub code path which you should upgrade to explicitly reinstate default certificate validation if you find that anything else in your app is using it.
Another different, useful approach to approving certificates only in contexts where you actually want to is in this SO answer.

Verify remote SSL certificate during HTTPS request

When making HTTPS request to remote web server, I use WebRequest, which establishes secure connection with remote web server. During development, I use self-signed cert on server, and WebRequest fails to establish secure connection, since cert is not valid, which is expected behavior.
I have found this code that "remotes" cert check, activated by calling SetCertificatePolicy() method in following code.
public static void SetCertificatePolicy()
{
ServicePointManager.ServerCertificateValidationCallback
+= RemoteCertificateValidate;
}
/// <summary>
/// Remotes the certificate validate.
/// </summary>
private static bool RemoteCertificateValidate(
object sender, X509Certificate cert,
X509Chain chain, SslPolicyErrors error)
{
// trust any certificate!!!
System.Console.WriteLine("Warning, trust any certificate");
return true;
}
I am wondering, if it is possible to do special checks on remote SSL cert (using above code, for instance), so that I can verify that remote web server uses valid SSL cert, and not just any valid cert, but exactly the one I want? For instance, I want to make sure that I'm talking to www.someplace.com website, cert issued to ACME Inc, with fingerprint 00:11:22:.....
What is a "best practice" approach for this scenario?
Thanks!
If you really want to nail it down to one particular certificate, you can compare the certificate data (in DER format) to the byte[] in certificate.GetRawCertData().
You can also use GetCertHashString() and Subject on the certificate parameter in RemoteCertificateValidate to get the information you're after. The hostname ought to be in the subject alternative name of the certificate or, if there isn't a subject alternative name, in the CN of the subject (distinguished) name. Considering the way .NET formats the subject string, this ought to be the first CN= you find there.
You'll also get more data if certificate is an instance of X509Certificate2. You'll then be able to get SubjectName as an X500PrincipalName and also the Extensions (to check the subject alternative name extension). It might be useful to use tools such as BouncyCastle for parsing the subject name.
You're also likely to get more information about the hostname you're trying to contact in the sender, depending on its runtime type.

Using intermediate certificates with SslStream and X509Certificate2 in a server app

I am working on a .Net server application that uses SslStream to provide its SSL sockets. It works with some clients (such as those based on libcurl), but other clients throw errors due to the lack of the intermediate certificate(s). How can I associate the intermediate certificate with the SslStream or X509Certificate2 object to make these clients happy?
Here's the code I'm using now, when accepting the connection:
X509Certificate2 cert = new X509Certificate2("cert.pfx", "");
theSslStream.BeginAuthenticateAsServer(cert, ...);
If I were using OpenSSL I'd do this with SSL_CTX_add_extra_chain_cert(). I've looked at the X509Chain object but don't see how to make it fit in.
Thanks.
Have you tried including the full chain in the pfx you're using (eg, use OpenSSL to plug them all in)? I haven't tried this specifically with SSLStream, but WCF doesn't provide an explicit way to include intermediate certs- it just presents the full chain automatically if the intermediate certs are available in the source .pfx.
Including the Intermediate certificates in the .pfx file is the solution. You can verify that all the correct Intermediate certificates are installed at http://www.sslshopper.com/ssl-checker.html

Categories

Resources