How exactly is a Certificate chain built? - c#

As the title states i want to know how the certificate chain is build internaly.
I have a self signed root certificate (CA) und an incoming user certificate, which supposedly has been signed by my root, which is what i have to test. Testing a X509Certificate2 can be done with the X509Certificate2.Verify() method, which uses the X509Chain with some standard settings. To have full control over the process i am using the X509Chain directly. As my root is not installed on the machine (not in trusted root certificate store) i am adding it to the ExtraStore of the ChainPolicy.
The whole magic happens within X509Chain.Build() method, where i pass my 'to be tested' user certificate. The chain gets build, starting with the passed certificate and ending with the self signed root. But how exactly? How does one test wether a certificate has been signed by one or another root? Does it use digital signatures internally?
I tried to debug the whole thing but ended at an DLL import, which uses the CertGetCertificateChain method from Libraries.Crypt32. That is C++ stuff, which exceeds my domain of knowledge.
Simplified code so far:
using (var chain = new X509Chain
{
ChainPolicy =
{
RevocationMode = X509RevocationMode.NoCheck,
RevocationFlag = X509RevocationFlag.ExcludeRoot,
VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority,
VerificationTime = DateTime.Now,
UrlRetrievalTimeout = new TimeSpan(0, 0, 0)
}
})
{
chain.ChainPolicy.ExtraStore.Add(authority);
if (!chain.Build(signedCertificate))
{
//Errorhandling
}
//Root certificate should always be last in the X509ChainElements collection
var isSignedByCorrectRootCert =
chain.ChainElements
.Cast<X509ChainElement>()
.Last()
.Certificate.Thumbprint == authority.Thumbprint;
if (!isSignedByCorrectRootCert)
{
//Errorhandling
}
}

Does it use digital signatures internally?
exactly.
I tried to debug the whole thing but ended at an DLL import, which uses the CertGetCertificateChain method from Libraries.Crypt32.
yes, .NET X509Chain class is nothing else than plain wrapper over CryptoAPI native (C++) functions. I would say that 95+% of cryptography stuff in .NET are wrappers over CryptoAPI functions.
Years ago I wrote a blog post that explains how chain building is performed in Microsoft Windows: Certificate Chaining Engine — how it works. This post explains how chaining engine builds the chain and bind certificates in the chain before sending it to validation routine. Chain validation is a much more complex process. Validation logic is described in RFC 5280, §6

Related

The remote certificate was rejected by the provided RemoteCertificateValidationCallback - how to get more details?

I have prepared a test case for my problem - a very simple .NET 6 Console app:
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace CheckCert
{
internal class Program
{
public const string jsonUrl = "https://wordsbyfarber.com/de/top-5";
static void Main(string[] args)
{
Console.WriteLine($"jsonUrl = {jsonUrl}");
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = BackendCaValidation,
CheckCertificateRevocationList = true,
};
HttpClient httpClient = new(handler);
string jsonStr = httpClient.GetStringAsync(new Uri(jsonUrl)).Result;
Console.WriteLine($"jsonStr = {jsonStr}");
}
private static bool BackendCaValidation(HttpRequestMessage message,
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
Console.WriteLine($"sslPolicyErrors = {sslPolicyErrors}");
return SslPolicyErrors.None == sslPolicyErrors;
}
}
}
When I run it, it works as expected, will print SslPolicyErrors.None and the JSON content from my private website, which uses a Let's Encrypt certificate.
However when I change the jsonUrl to the URL of my work server, which I am better not sharing in public, then I end up with SslPolicyErrors.RemoteCertificateChainErrors and a System.AggregateException.
The exception says "look at the inner exception".
So inspect the inner exception and it says:
The remote certificate was rejected by the provided RemoteCertificateValidationCallback
So I keep looking at the certificate and the chain displayed by the Microsoft Edge browser - both for my private website and for the work server.
The work server uses a certificate issued by a self-signed work CA (and there is an intermediate certificate inbetween). All 3 certificates are not expired yet.
My question is: how to get more information here?
Why exactly do I get a SslPolicyErrors.RemoteCertificateChainErrors? is it because of that self-signed corporate CA or maybe because of some signing algorithm?
And also - similar code works for us in another project (an Azure Service Fabric application) without failing. I wonder, what could be the difference?
UPDATE:
I have followed the suggestion by Mr. Spiller (thank you!) and have added the code:
Console.WriteLine("-----------------------------------");
foreach (X509ChainStatus status in chain.ChainStatus)
{
Console.WriteLine($"status = {status.Status}");
}
Now my private Let's Encrypt secured URL looks like this (why is there no chain printed? I can see the chain in the web browser):
And the "faulty" corporate URL looks like this:
My main question is: how to make my app work against the corporate URL, without making it insecure?
I.e. I would probably have to accept the returned SslPolicyErrors.RemoteCertificateChainErrors in my app, but can I still perform some checks?
The parameter X509Chain? chain has a property ChainStatus which you can use to get the status for each element of the certification chain.
Each element in turn has a property Status of type System.Security.Cryptography.X509Certificates.X509ChainStatusFlags (cf. documentation) that should give you the status of each particular element of the certification chain.
In your case one (the only?) element most likely has the status UntrustedRoot.
If you want to connect to your corporate server even though the certificate is not trusted, you can simply return true from the callback. I.e. in BackendCaValidation check whether you are talking to the corporate server and return true even though sslPolicyErrors is not None.
The other (preferred?) way is to trust your corporate CA system-wide. I.e. add the CA to the cert store of your operating system and mark it as trusted.

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).

X509Chain.Build() method explanation

I want to validate chain of certificates, I get a X509Certificate2 collection and have to validate if all the certificates build one chain.
Usually, in order to verify the certificates chain I should take the digital signature from the leaf certificate and check if it is signed by the root certificate - but in .NET I can't find a way to extract the signature from the X509Certificate2 object.
Therefore, I thought of using X509Chain.Build() method in the following way:
void ValidateChain(X509Certificate2Collection collection, X509Certificate2 leaf)
{
X509Chain x509Chain = new X509Chain();
x509Chain.ChainPolicy.ExtraStore.AddRange(collection);
bool isValid = x509Chain.Build(leaf);
}
But I have some questions about the build method:
As I understood, the chain was built also from my computer store, and I want that it is built only from the ExtraStore, how can I define this behaviour?
I saw that after the chain was built it doesn't contain the Root Certificate; my question is why, and how can I verify that the chain has Root CA, since this is not part of the chain elements.
I will so appreciate it if someone can explain to me how the Build() method works.
You should use the ChainStatus value after the Build operation. MSDN reference here:
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.
Try this code snippet out:
bool chainIsValid = false;
var chain = new X509Chain();
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 1, 0);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
chainIsValid = chain.Build(certificate);

Can I ensure, using C#, that an X509Certificate was issued by a trusted authority?

If I use X509Certificate.CreateFromSignedFile to get the certificate used to sign a file, can I confirm that it was signed by a trusted authority - and isn't just a "self-signed" cert of some kind?
I want to extract the "Subject" (company) name from the cert to ensure that an unmanaged DLL I'm using is unmolested (I can't checksum it as it's updated frequently and independently) and official.
However, I'm concerned that a fake DLL could be signed with a "self-signed" cert and return the original company's name. So, I want to ensure the the cert was issued by Versign, Thwate or similar (anything installed on the cert repository on the machine will be fine).
How can I do this, if at all, when using X509Certificate.CreateFromSignedFile? Or does it do this automatically (i.e. a "self-signed" cert will fail)?
If it is not valid certificate you will get an exception. What concerns that you want to check the Company name and etc...
Here is the code :
ServicePointManager.ServerCertificateValidationCallback +=
new System.Net.Security.RemoteCertificateValidationCallback(customXertificateValidation);
private static bool customXertificateValidation(
object sender, X509Certificate cert,
X509Chain chain, System.Net.Security.SslPolicyErrors error)
{
// check here 'cert' parameter properties (ex. Subject) and based on the result
// you expect return true or false
return false/true;
}
EDIT : The above code is suitable only when requesting https resource which is got not valid(self-signed, expired...etc) certificate. What concerns extracting signatures from signed files please check here : Extracting Digital Signatures from Signed Files with .NET
Isn't Verify() method enough?

Generate and sign certificate in different machines C#

I need to generate certificates to be used in secure communication between agents. Each agent generates a certificate and must send it to the system CA, in another machine, to be signed (and trusted by the other agents). I am doing it using C# with the following code for the agent:
//generate certificate
ECDsa elipticCurveNistP256Key = ECDsa.Create(ECCurve.CreateFromValue("1.2.840.10045.3.1.7")); // nistP256 curve
CertificateRequest certificateRequest = new CertificateRequest("CN=" + agentId, elipticCurveNistP256Key, HashAlgorithmName.SHA256);
certificateRequest.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
certificateRequest.CertificateExtensions.Add(
new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation,
false));
// Add the SubjectAlternativeName extension
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddIpAddress(IPAddress.Parse(agentIpAddress));
certificateRequest.CertificateExtensions.Add(sanBuilder.Build());
certificateRequest.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection
{
new Oid("1.3.6.1.5.5.7.3.8")
},
true));
certificateRequest.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(certificateRequest.PublicKey, false));
And the following code for the CA system:
X509Certificate2 signedCertificate = certificateRequest.Create(
caCertificatePFX,
DateTimeOffset.UtcNow.AddDays(-1),
DateTimeOffset.UtcNow.AddDays(30),
new byte[] {1, 2, 3, 4});
Of course, I use also code for communication between the machines that I do not show here. But I have at least two problems:
I would like to have a complete separation between certificate generation and signing but even with lots of tries this was the only code that I could manage to get to work. If I am not mistaken this code has the certificate creation at the CA system which is not the ideal scenario (CA has access to agent private key) but if I didn't find a better one it's something I can accept.
The second problem is that even if I accept the first problem I still need to send the CertificateRequest object from one machine to another and CertificateRequest is not serializable. I have found the method CreateSigningRequest() that "Creates an ASN.1 DER-encoded PKCS#10 CertificationRequest value representing the state of the current object." however I have not found a way to then make it be a CertificateRequest object again so that I can run the CA system code.
Does anyone know how I can do this? Hopefully to completely separate certificate generation and certificate signing, but if that is not possible at least to create CertificateRequest object back.
I am running .Net Framework 4.7.2 that I need to maintain in order to use previously developed Windows Forms.
Thanks
As you noted, there's not a way to read back the PKCS#10 request. That's largely because too many of the things are missing to be an "OK" Certificate Authority, so having a reader would just make for a lot of "bad" Certificate Authorities. (Since your CA doesn't support revocation it's also a "bad" CA, but you're mitigating that with short lifetime certificates.)
The PKCS#10 request contains:
A data format version
A name (presumably the one that the requester wants)
A public key
Attributes
The requested extensions comes here (EKUs, Subject Alternative Name, etc)
A signature, to prove that the requester has the private key.
The data format version is irrelevant if you're not using the data format, and the signature is not really important for "closed" issuers (CAs that only issue certificates to directly-known parties). So you just need to transport the public key and whatever other data you need for the request (looking at your current code, the agent ID and IP address).
The only tricky part is sending the public key... but with .NET Core 3.0+ you can normalize all of the keys to their SubjectPublicKeyInfo format:
byte[] spki = elipticCurveNistP256Key.ExportSubjectPublicKeyInfo();
While it would have been exceedingly clever for the PublicKey type to have an ImportSubjectPublicKeyInfo method, that hasn't happened yet. For generic parsing you'd want to try all the major key types, but since you're a closed CA on the other side you can know a priori that it's ECDSA:
using (ECDsa clientPub = ECDsa.Create())
{
clientPub.ImportSubjectPublicKeyInfo(transmittedSpki, out _);
// the rest of your code goes here.
}
I would strongly suggest to use CA software to sign certificate requests. Period.
Any attempt to roll own your CA code will make the solution unreliable, fragile and error-prone in many aspects. There are several options, starting with Microsoft ADCS (Windows) and EJBCA (Windows/Linux). Any other design will be simply bad.

Categories