开发者

How to verify X509 cert without importing root cert?

开发者 https://www.devze.com 2023-03-07 15:35 出处:网络
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.

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.


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


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.


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();
        }
    }
}


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:

How to verify X509 cert without importing root cert?

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


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
}
0

精彩评论

暂无评论...
验证码 换一张
取 消