How to verify X509 cert without importing root cert?
Asked Answered
S

5

24

My program contains 2 root certs I know and trust. I have to verify certs of trustcenters and "user" certs issued by the trustcenters which all originate from these 2 root certs.

I use X509Chain class to verify but that only works if the root cert is in the windows certificate store.

I'm looking for a way to verify the certs without importing theeses root certs - somehow tell the X509Chain class that I do trust this root certs and it should check just the certs in the chain and nothing else.

Actual code:

        X509Chain chain = new X509Chain();
        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
        chain.ChainPolicy.ExtraStore.Add(root); // i do trust this
        chain.ChainPolicy.ExtraStore.Add(trust);
        chain.Build(cert);

Edit: It's a .NET 2.0 Winforms application.

Sherlocke answered 23/5, 2011 at 13:13 Comment(1)
What about using Bouncy Castle C# PkixCertPathBuilder API?.Prolific
Y
17

I opened an Issue on dotnet/corefx and they replied as follows:

If AllowUnknownCertificateAuthority is the only flag set then chain.Build() will return true if

  • The chain correctly terminated in a self-signed certificate (via ExtraStore, or searched persisted stores)

  • None of the certificates are invalid per the requested revocation policy

  • All of the certificates are valid under the (optional) ApplicationPolicy or CertificatePolicy values

  • All of the certificates' NotBefore values are at-or-before VerificationTime and all of the certificates' NotAfter values are (at-or-)after VerificationTime.

If that flag is not specified then an additional constraint is added:

The self-signed certificate must be registered as trusted on the system (e.g. in the LM\Root store).

So, Build() returns true, you know that a time-valid non-revoked chain is present. The thing to do at that point is read chain.ChainElements[chain.ChainElements.Count - 1].Certificate and determine if it is a certificate that you trust. I recommend comparing chainRoot.RawData to a byte[] representing a certificate that you trust as a root in context (that is, byte-for-byte compare rather than using a thumbprint value).

(If other flags are set then other constraints are also relaxed)

So you should do it this way:

X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.ExtraStore.Add(root);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
var isValid = chain.Build(cert);

var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
isValid = isValid && chainRoot.RawData.SequenceEqual(root.RawData);
Yb answered 11/6, 2018 at 22:57 Comment(2)
The first bullet does not appear to be true. I've tested with a certificate with an unknown issuer, and it still returns true. The chain status still correctly identifies the problem though.Baylor
Apparently it's not true. The person who responded to David's issue clarified in a new issue that AllowUnknownCertificateAuthority suppresses partial chain issues too.Norland
C
10

EDIT

Over the years we found several issues with the original X509Chain solution I had posted here due to X509Chain performing incorrect behaviors for certain edge cases. Thus I can no longer recommend using X509Chain for this problem. Our product has since moved to using Bouncy Castle to do all of our certificate chain verification and it has held up to all of our testing and always works as expected.

The basis of our new solution can be found here: Build certificate chain in BouncyCastle in C#

I have removed the original answer so no one is using a bad security solution.

Clarinda answered 30/9, 2015 at 17:20 Comment(3)
This seems to also return "untrusted root" even if the root used to sign the end-entity cert is different. Not really wanted behaviour.Terat
I agree with your comments about AllowUnknownCertificateAuthority, I came to the same conclusion myself whilst adding my own CA certs to the ExtraStoreWallis
Actually X509ChainStatusFlags.UntrustedRoot is returned even if no CA certificate is added, which makes the answer unacceptable.Interne
M
1

The way to obtain this would be to write a custom validation.

If you are in a WCF context this is done by subclassing the System.IdentityModel.Selectors.X509CertificateValidator and specifying the custom validation on the serviceBehavior object in web.config:

<serviceBehaviors>
    <behavior name="IdentityService">
      <serviceMetadata httpGetEnabled="true" />
      <serviceDebug includeExceptionDetailInFaults="true" />
      <serviceCredentials>
        <clientCertificate>
          <authentication customCertificateValidatorType="SSOUtilities.MatchInstalledCertificateCertificateValidator, SSOUtilities"
            certificateValidationMode="Custom" />
        </clientCertificate>
        <serviceCertificate findValue="CN=SSO ApplicationManagement"
          storeLocation="LocalMachine" storeName="My" />
      </serviceCredentials>
    </behavior>

But if you are just looking at a way to accept SSL certs from another host you can modify the system.net settings in the web.config file:

Below is an example of a X509CertificateValidator that tests if the clients cert is present in the LocalMachine/Personal store. (Which is not what you need but might be useful as an example.

using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Security.Cryptography.X509Certificates;

/// <summary>
/// This class can be injected into the WCF validation 
/// mechanism to create more strict certificate validation
/// based on the certificates common name. 
/// </summary>
public class MatchInstalledCertificateCertificateValidator
    : System.IdentityModel.Selectors.X509CertificateValidator
{
    /// <summary>
    /// Initializes a new instance of the MatchInstalledCertificateCertificateValidator class.
    /// </summary>
    public MatchInstalledCertificateCertificateValidator()
    {
    }

    /// <summary>
    /// Validates the certificate. Throws SecurityException if the certificate
    /// does not validate correctly.
    /// </summary>
    /// <param name="certificateToValidate">Certificate to validate</param>
    public override void Validate(X509Certificate2 certificateToValidate)
    {
        var log = SSOLog.GetLogger(this.GetType());
        log.Debug("Validating certificate: "
            + certificateToValidate.SubjectName.Name
            + " (" + certificateToValidate.Thumbprint + ")");

        if (!GetAcceptedCertificates().Where(cert => certificateToValidate.Thumbprint == cert.Thumbprint).Any())
        {
            log.Info(string.Format("Rejecting certificate: {0}, ({1})", certificateToValidate.SubjectName.Name, certificateToValidate.Thumbprint));
            throw new SecurityException("The certificate " + certificateToValidate
                + " with thumprint " + certificateToValidate.Thumbprint
                + " was not found in the certificate store");
        }

        log.Info(string.Format("Accepting certificate: {0}, ({1})", certificateToValidate.SubjectName.Name, certificateToValidate.Thumbprint));
    }

    /// <summary>
    /// Returns all accepted certificates which is the certificates present in 
    /// the LocalMachine/Personal store.
    /// </summary>
    /// <returns>A set of certificates considered valid by the validator</returns>
    private IEnumerable<X509Certificate2> GetAcceptedCertificates()
    {
        X509Store k = new X509Store(StoreName.My, StoreLocation.LocalMachine);

        try
        {
            k.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
            foreach (var cert in k.Certificates)
            {
                yield return cert;
            }
        }
        finally
        {
            k.Close();
        }
    }
}
Megdal answered 23/5, 2011 at 13:26 Comment(1)
I've edited the question, that it's a .NET 2.0 Winforms application.Sherlocke
H
1

If you know which certificates can be root and intermediate certificates for the certificate to check, you can load the public keys of the root and intermediate certificates in the ChainPolicy.ExtraStore collection of the X509Chain object.

My task was also to write a Windows Forms application to install a certificate, only if it was issued dependent on the known "National Root certificate" of my country's government. There also is a limited number of CA's that are allowed to issue certificates to authenticate connections to the national web services, so I had a limited set of certificates that can be in the chain and might be missing on the target machine. I collected all public keys of the CA's and the government root certificates in a subdirectory "cert" of the application: chain certificates

In Visual Studio, I added the directory cert to the solution and marked all files in this directory as embedded resource. This allowed me to enumerate the collection of "trusted" certificates in my c# library code, to build a chain to check the certificate even if the issuer certificate is not installed. I made a wrapper class for X509Chain for this purpose:

private class X509TestChain : X509Chain, IDisposable
{
  public X509TestChain(X509Certificate2 oCert)
    : base(false)
  {
    try
    {
      ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
      ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
      if (!Build(oCert) || (ChainElements.Count <= 1))
      {
        Trace.WriteLine("X509Chain.Build failed with installed certificates.");
        Assembly asmExe = System.Reflection.Assembly.GetEntryAssembly();
        if (asmExe != null)
        {
          string[] asResources = asmExe.GetManifestResourceNames();
          foreach (string sResource in asResources)
          {
            if (sResource.IndexOf(".cert.") >= 0)
            {
              try
              {
                using (Stream str = asmExe.GetManifestResourceStream(sResource))
                using (BinaryReader br = new BinaryReader(str))
                {
                  byte[] abResCert = new byte[str.Length];
                  br.Read(abResCert, 0, abResCert.Length);
                  X509Certificate2 oResCert = new X509Certificate2(abResCert);
                  Trace.WriteLine("Adding extra certificate: " + oResCert.Subject);
                  ChainPolicy.ExtraStore.Add(oResCert);
                }
              }
              catch (Exception ex)
              {
                Trace.Write(ex);
              }
            }
          }
        }
        if (Build(oCert) && (ChainElements.Count > 1))
          Trace.WriteLine("X509Chain.Build succeeded with extra certificates.");
        else
          Trace.WriteLine("X509Chain.Build still fails with extra certificates.");
      }
    }
    catch (Exception ex)
    {
      Trace.Write(ex);
    }
  }

  public void Dispose()
  {
    try
    {
      Trace.WriteLine(string.Format("Dispose: remove {0} extra certificates.", ChainPolicy.ExtraStore.Count));
      ChainPolicy.ExtraStore.Clear();
    }
    catch (Exception ex)
    {
      Trace.Write(ex);
    }
  }
}

In the calling function, I could now successfully check if an unknown certificate derives from the national root certificate:

    bool bChainOK = false;
    using (X509TestChain oChain = new X509TestChain(oCert))
    {
      if ((oChain.ChainElements.Count > 0)
        && IsPKIOverheidRootCert(oChain.ChainElements[oChain.ChainElements.Count - 1].Certificate))
        bChainOK = true;
      if (!bChainOK)
      {
        TraceChain(oChain);
        sMessage = "Root certificate not present or not PKI Overheid (Staat der Nederlanden)";
        return false;
      }
    }
    return true;

To complete the picture: to check the root certificate (that usually is installed because it is included in Windows Update, but in theory could be missing as well), I compare the friendly name and thumbprint to the published values:

private static bool IsPKIOverheidRootCert(X509Certificate2 oCert)
{
  if (oCert != null)
  {
    string sFriendlyName = oCert.FriendlyName;
    if ((sFriendlyName.IndexOf("Staat der Nederlanden") >= 0)
      && (sFriendlyName.IndexOf(" Root CA") >= 0))
    {
      switch (oCert.Thumbprint)
      {
        case "101DFA3FD50BCBBB9BB5600C1955A41AF4733A04": // Staat der Nederlanden Root CA - G1
        case "59AF82799186C7B47507CBCF035746EB04DDB716": // Staat der Nederlanden Root CA - G2
        case "76E27EC14FDB82C1C0A675B505BE3D29B4EDDBBB": // Staat der Nederlanden EV Root CA
          return true;
      }
    }
  }
  return false;
}

I am not sure if this check is secure at all, but in my case the operator of the Windows Forms application is quite sure to have access to a valid certificate to be installed. The goal of the software is just to filter the certificates list to help him install only the correct certificate in the machine store of the computer (the software also installs the public keys of the intermediate and root certificate, to ensure that the runtime behavior of the web service client is correct).

Henni answered 29/3, 2013 at 6:53 Comment(0)
R
1

I just extended the code from @Tristan with a check that the root certificate is one of the certificates added to the ExtraStore.

X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.ExtraStore.Add(root);
chain.Build(cert);
if (chain.ChainStatus.Length == 1 &&
    chain.ChainStatus.First().Status == X509ChainStatusFlags.UntrustedRoot &&
    chain.ChainPolicy.ExtraStore.Contains(chain.ChainElements[chain.ChainElements.Count - 1].Certificate))
{
    // chain is valid, thus cert signed by root certificate 
    // and we expect that root is untrusted which the status flag tells us
    // but we check that it is a known certificate
}
else
{
    // not valid for one or more reasons
}
Ralli answered 16/12, 2016 at 10:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.