How to fully validate a X509 certificate? - c#

I need to validate a certificate(X509Certificate2) the same way it´s validated before it is used for communication.
The X509Certificate2.Verify() will in this case return true while the certificate is not issued to the server it is installed on.
Is there any finished code block to do a full validation of the X509 certificate?
Regards
Edit : This is the code I have tried with :
var certificate = GetServerCertificate(CertificateStore,CertificateLocation,Thumbprint);
if(certificate != null)
{
if(certificate.Verify())
_logger.Log(NLog.LogLevel.Info, $"Yes");
else
_logger.Log(NLog.LogLevel.Info, $"No");
}

The X509Certificate2.Verify() will in this case return true while the certificate is not issued to the server it is installed on.
The Verify method doesn't check anything about hostnames. It verifies that
The certificate is not expired.
The certificate eventually chains to a trusted root authority.
All certificates in the chain have appropriately nested expiration.
The target certificate, unless it's self-issued, has a revocation endpoint, and is not revoked.
Any intermediate certificates have revocation endpoints and are not revoked.
It's exactly equal to
using (X509Chain chain = new X509Chain())
{
// Use the default vales of chain.ChainPolicy including:
// RevocationMode = X509RevocationMode.Online
// RevocationFlag = X509RevocationFlag.ExcludeRoot
// VerificationFlags = X509VerificationFlags.NoFlag
// VerificationTime = DateTime.Now
// UrlRetrievalTimeout = new TimeSpan(0, 0, 0)
bool verified = chain.Build(cert);
for (int i = 0; i < chain.ChainElements.Count; i++)
{
chain.ChainElements[i].Certificate.Dispose();
}
return verified;
}
Is there any finished code block to do a full validation of the X509 certificate?
If "full validation" just means all the things Verify does, then yes. If you also care that it's valid for use as a TLS client certificate (or TLS server certificate) then you would use the longer form (using X509Chain directly) and add an application policy requirement before calling chain.Build:
// See if it's valid as a TLS server
chain.ChainPolicy.ApplicationPolicy.Add(new Oid("1.3.6.1.5.5.7.3.1"));
// Alternatively, if it's valid as a TLS client
chain.ChainPolicy.ApplicationPolicy.Add(new Oid("1.3.6.1.5.5.7.3.2"));
The hostname is much harder. Client certificates don't have validatable names, it's just up to what the server does with it. Server certificates have hostname matching against SAN/Subject-CN, but there's nothing built-in that does that check other than just connecting with TLS (SslStream).

Related

C# X509 certificate validation, with Online CRL check, without importing root certificate to trusted root CA certificate store

I'm trying to validate an X509 certificate chain without importing the root CA certificate into the trusted root CA certificate store (in production this code will run in an Azure Function, and you can't add certificates to the trusted root CA certificate store on Azure App Services).
We also need to perform an online CRL check on this certificate chain.
I've searched on this and I see many others are facing the same problem, but none of the suggestions seem to work. I've followed the approach outlined in this SO post, which echoes the suggestions from issue #26449 on the dotnet/runtime GitHub. Here's a small console application (targetting .NET Core 3.1) reproducing the problem:
static void Main(string[] args)
{
var rootCaCertificate = new X509Certificate2("root-ca-cert.cer");
var intermediateCaCertificate = new X509Certificate2("intermediate-ca-cert.cer");
var endUserCertificate = new X509Certificate2("end-user-cert.cer");
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(rootCaCertificate);
chain.ChainPolicy.ExtraStore.Add(intermediateCaCertificate);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.Build(endUserCertificate);
chain.Build(new X509Certificate2(endUserCertificate));
var errors = chain.ChainStatus.ToList();
if (!errors.Any())
{
Console.WriteLine("Certificate is valid");
return;
}
foreach (var error in errors)
{
Console.WriteLine($"{error.Status.ToString()}: {error.StatusInformation}");
}
}
When ran this returns three errors:
UntrustedRoot: A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.
RevocationStatusUnknown: The revocation function was unable to check revocation for the certificate.
OfflineRevocation: The revocation function was unable to check revocation because the revocation server was offline.
However, if I add the root CA certificate to the trusted root CA certificate store then all three errors disappear.
Questions
Is this something wrong with my implementation, or is what I'm trying to do not possible?
What are my options to try to achieve this? A bit of Googling suggests the X509ChainPolicy.CustomTrustStore offered in .NET 5 might save the day. Is Bouncy Castle another option for achieving this?
A bit of Googling suggests the X509ChainPolicy.CustomTrustStore offered in .NET 5 might save the day
Yep.
Instead of putting rootCaCertificate into ExtraStore, put it into CustomTrustStore, then set chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;. Now your provided root is the only root valid for the chain. You can also remove the AllowUnknownCertificateAuthority flag.
OfflineRevocation
This error is slightly misleading. It means "revocation was requested for the chain, but one or more revocation responses is missing". In this case it's missing because the builder didn't ask for it, because it didn't trust the root. (Once you don't trust the root you can't trust the CRLs/OCSP responses, so why ask for them at all?)
RevocationStatusUnknown
Again, the unknown is because it didn't ask for it. This code is different than OfflineRevocation because technically a valid OCSP response is (effectively) "I don't know". That'd be an online/unknown.
UntrustedRoot
Solved by the custom trust code above.
Other things of note: The correct way to determine the certificate is valid is to capture the boolean return value from chain.Build. For your current chain, if you had disabled revocation (chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck) then Build would have returned true... but the UntrustedRoot error would still be present in the ChainStatus output. The boolean return from Build is false if there are any errors that the VerificationFlags didn't say to ignore.
You also only need to call Build once :).
static void Main(string[] args)
{
var rootCaCertificate = new X509Certificate2("root-ca-cert.cer");
var intermediateCaCertificate = new X509Certificate2("intermediate-ca-cert.cer");
var endUserCertificate = new X509Certificate2("end-user-cert.cer");
var chain = new X509Chain();
chain.ChainPolicy.CustomTrustStore.Add(rootCaCertificate);
chain.ChainPolicy.ExtraStore.Add(intermediateCaCertificate);
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
bool success = chain.Build(endUserCertificate);
if (success)
{
return;
}
foreach (X509ChainStatus error in chain.ChainStatus)
{
Console.WriteLine($"{error.Status.ToString()}: {error.StatusInformation}");
}
}
Well, not a full answer, but probably it will get you going.
We've built before an Azure Web App (not a function app, but I guess wouldn't matter) which did exactly what you want. Took us a week or so. Full answer would be posting the code of the whole app, we I obviously cannot do. Some hints though.
We walked around the certificate problem by uploading certificates to the app (TLS settings) and accessing them in code through WEBSITE_LOAD_CERTIFICATES setting (you put thumbprints there, it's also mentioned in the link you posted), than you can get them in code and build your own certificate store:
var certThumbprintsString = Environment.GetEnvironmentVariable("WEBSITE_LOAD_CERTIFICATES");
var certThumbprints = certThumbprintsString.Split(",").ToList();
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
for (var i = 0; i < certThumbprints.Count; i++)
{
store.Certificates.Find(X509FindType.FindByThumbprint, certThumbprint, false);
}
Than we implemented a number of validators: for subject, time, certificate chain (you basically mark your root somehow and go from the certificate you validate up the chain in your store and see if you end up in your root) and CRL.
For CRL Bouncy Castle offers support:
// Get CRL from certificate, fetch it, cache it
var crlParser = new X509CrlParser();
var crl = crlParser.ReadCrl(data);
var isRevoked = crl.IsRevoked(cert);
Getting CRL from the certificate is tricky, but doable (I followed this for the purpose, more or less https://learn.microsoft.com/en-us/archive/blogs/joetalksmicrosoft/pki-authentication-as-a-azure-web-app).

Mimekit installing certificates upon verify

I am using MimeKit to verify that an SMIME multipart signed message is signed by a specific entity. I have the public certificate for this entity. When calling Verify(context) on the MultipartSigned object, the certificate is being imported into the servers certificate list. What I want is that this errors if the certificate is not found. otherwise if a malicious user signs a file it would pass through, and what's worse I would end up with his certificate installed.
Is my reasoning flawed here?
This is the code I'm using.
var signed =(MultipartSigned)MimeEntity.Load(ParserOptions.Default,#"C:\mysignedfile.txt");
using (var context = new WindowsSecureMimeContext(StoreLocation.LocalMachine))
{
foreach (var signature in signed.Verify(context))// This install the certificate!
{
try
{
bool valid = signature.Verify();
}
catch (DigitalSignatureVerifyException)
{
throw;
// There was an error verifying the signature.
}
}
}
Having the certificate in your certificate store does not mean that it is trusted, it just means that it is known.
That's all.
This allows you to later mark it as trusted and also allows your system to check for revocations.

SslStream authentication failure

everyone, I'm trying to write something about SSL and here's the question:
I've built things below:
CA certs (a self-made CA)
Server pfx, Server cert, Server key (signed by the self-made CA to "localhost")
Now I'm using .Net SslStream to test the connection:(Client and Server are in different thread, and the TCP connection was built already)
Client:
sslStream.AuthenticateAsClient("localhost");
Server:
sslStream.AuthenticateAsServer(serverCert);
//serverCert is X509Certificate2 built from "server.pfx"
the client's AuthenticateAsClient Method will throw a Exception
"The remote certificate is invalid according to the validation procedure."
I guess the reason is that the Server's certificate is signed by a untrusted CA, so the authentication failed, then how could I add the CA certificate to my trust list?
I tried to add code below in client code, but it won't work
X509Store store = new X509Store(StoreName.TrustedPublisher, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(new X509Certificate2(Resources.CACertPath));
store.Close();
sslStream.AuthenticateAsClient("localhost");
The following code will avoid the Windows certificate stores and validate the chain.
I see no reason to add the CA certificate needed to verify a chain to the hundreds in the certificate store already. That means Windows will try to verify the chain with "hundreds + 1" certificates rather than the one certificate truly required.
I have not figured out how to use this chain (chain2 below) by default such that there's no need for the callback. That is, install it on the ssl socket and the connection will "just work". And I have not figured out how install it such that its passed into the callback. That is, I have to build the chain for each invocation of the callback. I think these are architectural defects in .Net, but I might be missing something obvious.
The name of the function does not matter. Below, VerifyServerCertificate is the same callback as RemoteCertificateValidationCallback in SslStream class. You can also use it for the ServerCertificateValidationCallback in ServicePointManager.
static bool VerifyServerCertificate(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
try
{
String CA_FILE = "ca-cert.der";
X509Certificate2 ca = new X509Certificate2(CA_FILE);
X509Chain chain2 = new X509Chain();
chain2.ChainPolicy.ExtraStore.Add(ca);
// Check all properties
chain2.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
// This setup does not have revocation information
chain2.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// Build the chain
chain2.Build(new X509Certificate2(certificate));
// Are there any failures from building the chain?
if (chain2.ChainStatus.Length == 0)
return true;
// If there is a status, verify the status is NoError
bool result = chain2.ChainStatus[0].Status == X509ChainStatusFlags.NoError;
Debug.Assert(result == true);
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return false;
}

What do the different status error codes for the CheckValidationResult certificateProblem parameter stand for?

In the MSDN Example of the ICertificatePolicy interface, there is an enum which lists all the possible status error codes that can be returned via the ValidationCallback:
public enum CertificateProblem : long
{
CertEXPIRED = 0x800B0101,
CertVALIDITYPERIODNESTING = 0x800B0102,
CertROLE = 0x800B0103,
CertPATHLENCONST = 0x800B0104,
CertCRITICAL = 0x800B0105,
CertPURPOSE = 0x800B0106,
CertISSUERCHAINING = 0x800B0107,
CertMALFORMED = 0x800B0108,
CertUNTRUSTEDROOT = 0x800B0109,
CertCHAINING = 0x800B010A,
CertREVOKED = 0x800B010C,
CertUNTRUSTEDTESTROOT = 0x800B010D,
CertREVOCATION_FAILURE = 0x800B010E,
CertCN_NO_MATCH = 0x800B010F,
CertWRONG_USAGE = 0x800B0110,
CertUNTRUSTEDCA = 0x800B0112
}
What do these different status codes stand for? They are not in the SSPI Status Code document that the links reference.
I spent a long time trying to figure this out, and was finally able to find a VMware C# file containing explanations of the different codes:
CertEXPIRED
A required certificate is not within its validity period
CertVALIDITYPERIODNESTING
The validity periods of the certification chain do not nest correctly
CertROLE
A certificate that can only be used as an end-entity is being used as a CA or visa versa
CertPATHLENCONST
A path length constraint in the certification chain has been violated
CertCRITICAL
An extension of unknown type that is labeled 'critical' is present in a certificate
CertPURPOSE
A certificate is being used for a purpose other than that for which it is permitted
CertISSUERCHAINING
A parent of a given certificate in fact did not issue that child certificate
CertMALFORMED
A certificate is missing or has an empty value for an important field, such as a subject or issuer name
CertUNTRUSTEDROOT
A certification chain processed correctly, but terminated in a root certificate which isn't trusted by the trust provider
CertCHAINING
A chain of certs didn't chain as they should in a certain application of chaining
CertREVOKED
A certificate was explicitly revoked by its issuer
CertUNTRUSTEDTESTROOT
The root certificate is a testing certificate and the policy settings disallow test certificates
CertREVOCATION_FAILURE
The revocation process could not continue - the certificate(s) could not be checked
CertCN_NO_MATCH
The certificate's CN name does not match the passed value
CertWRONG_USAGE
The certificate is not valid for the requested usage
CertUNTRUSTEDCA
Untrusted CA

Verify Remote Server X509Certificate using CA Certificate File

I've generated a CA and multiple certificates (signed by CA) using OpenSSL and I have a .NET/C# client and server both using SslStream which each have their own certificates/keys, mutual authentication is enabled and revocation is disabled.
I'm using RemoteCertificateValidationCallback for SslStream to validate the remote server's certificate and I was hoping I could just load the CA's public certificate (as a file) in the program and use it to verify the remote certificate rather then actually installing the CA in the Windows Certificate Store. The problem is the X509Chain won't show anything else unless I install the CA into the store, either will the Windows CryptoAPI shell when I open a PEM version of one of the certificates.
My question is, how can I verify a certificate has been signed by my specific CA just by using the CA's public certificate file without using Windows certificate store or WCF when RemoteCertificateValidationCallback, X509Certificate and X509Chain don't seem to give me anything to work with?
Because the CA certificate is NOT in the root certificate store, you will have within the RemoteCertificateValidationCallback() an error flag of SslPolicyErrors.RemoteCertificateChainErrors ; a possibility is to validate explicitely the certificate chain against your own X509Certificate2Collection, since you are not using the local store.
if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
{
X509Chain chain0 = new X509Chain();
chain0.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// add all your extra certificate chain
chain0.ChainPolicy.ExtraStore.Add(new X509Certificate2(PublicResource.my_ca));
chain0.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
isValid = chain0.Build((X509Certificate2)certificate);
}
You can also re-use the chain passed in the callback, add your extra certificate(s) in the ExtraStore collection, and validate with the AllowUnknownCertificateAuthority flag which is needed since you add untrusted certificate(s) to the chain.
You could also prevent the original error by adding programmatically the CA certificate in the trusted root store (of course it opens a popup, for it is a major security problem to globally add a new trusted CA root) :
var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
X509Certificate2 ca_cert = new X509Certificate2(PublicResource.my_ca);
store.Add(ca_cert);
store.Close();
EDIT: For those who want to clearly test the chain with your CA :
Another possibility is to use the library BouncyCastle to build the certificate chain and validate the trust. The options are clear and errors are easy to understand. In cas of success it will build the chain, otherwise an exception is returned. Sample below :
// rootCerts : collection of CA
// currentCertificate : the one you want to test
var builderParams = new PkixBuilderParameters(rootCerts,
new X509CertStoreSelector { Certificate = currentCertificate });
// crls : The certificate revocation list
builderParams.IsRevocationEnabled = crls.Count != 0;
// validationDate : probably "now"
builderParams.Date = new DateTimeObject(validationDate);
// The indermediate certs are items necessary to create the certificate chain
builderParams.AddStore(X509StoreFactory.Create("Certificate/Collection", new X509CollectionStoreParameters(intermediateCerts)));
builderParams.AddStore(X509StoreFactory.Create("CRL/Collection", new X509CollectionStoreParameters(crls)));
try
{
PkixCertPathBuilderResult result = builder.Build(builderParams);
return result.CertPath.Certificates.Cast<X509Certificate>();
...
How can I verify a certificate has been signed by my specific CA just by using the CA's public certificate file without using Windows certificate store or WCF when RemoteCertificateValidationCallback, X509Certificate and X509Chain don't seem to give me anything to work with?
The following code will avoid the Windows certificate stores and validate the chain. Its a little different than JB's code, especially in the use of flags. The code below does not require AllowUnknownCertificateAuthority (but it does use X509RevocationMode.NoCheck since I don't have a CRL).
The name of the function does not matter. Below, VerifyServerCertificate is the same callback as RemoteCertificateValidationCallback in SslStream class. You can also use it for the ServerCertificateValidationCallback in ServicePointManager.
static bool VerifyServerCertificate(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
try
{
String CA_FILE = "ca-cert.der";
X509Certificate2 ca = new X509Certificate2(CA_FILE);
X509Chain chain2 = new X509Chain();
chain2.ChainPolicy.ExtraStore.Add(ca);
// Check all properties
chain2.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
// This setup does not have revocation information
chain2.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// Build the chain
chain2.Build(new X509Certificate2(certificate));
// Are there any failures from building the chain?
if (chain2.ChainStatus.Length == 0)
return true;
// If there is a status, verify the status is NoError
bool result = chain2.ChainStatus[0].Status == X509ChainStatusFlags.NoError;
Debug.Assert(result == true);
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return false;
}
I have not figured out how to use this chain (chain2 below) by default such that there's no need for the callback. That is, install it on the ssl socket and the connection will "just work". And I have not figured out how install it such that its passed into the callback. That is, I have to build the chain for each invocation of the callback. I think these are architectural defects in .Net, but I might be missing something obvious.

Categories