Mimekit installing certificates upon verify - c#

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.

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

How to fully validate a X509 certificate?

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

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

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?

Categories