C#如何验证根CA证书(x509)链?

26

假设我有三个证书(以Base64格式)

Root
 |
 --- CA
     |
     --- Cert (client/signing/whatever)

如何在C#中验证证书和证书路径/链?(这三个证书可能不在我的计算机证书存储中)

编辑:BouncyCastle有验证的功能。但我正在尝试不使用任何第三方库。

    byte[] b1 = Convert.FromBase64String(x509Str1);
    byte[] b2 = Convert.FromBase64String(x509Str2);
    X509Certificate cer1 = 
        new X509CertificateParser().ReadCertificate(b1);
    X509Certificate cer2 =
        new X509CertificateParser().ReadCertificate(b2);
    cer1.Verify(cer2.GetPublicKey());

如果cer1未被cert2(CA或根证书)签名,将会出现异常。这正是我想要的。

2个回答

41

你可以使用 X509Chain 类来实现这个功能,而且你还可以自定义它执行证书链构建的过程。

static bool VerifyCertificate(byte[] primaryCertificate, IEnumerable<byte[]> additionalCertificates)
{
    var chain = new X509Chain();
    foreach (var cert in additionalCertificates.Select(x => new X509Certificate2(x)))
    {
        chain.ChainPolicy.ExtraStore.Add(cert);
    }

    // You can alter how the chain is built/validated.
    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreWrongUsage;

    // Do the validation.
    var primaryCert = new X509Certificate2(primaryCertificate);
    return chain.Build(primaryCert);
}

如果需要,X509ChainBuild() == false 后将包含有关验证失败的其他信息。

编辑: 这只是确保您的 CA 是有效的。如果您想确保链是相同的,您可以手动检查指纹。您可以使用以下方法确保认证链是正确的,它期望链的顺序为:..., INTERMEDIATE2, INTERMEDIATE1 (INTERMEDIATE2 的签名者), CA (INTERMEDIATE1 的签名者)

static bool VerifyCertificate(byte[] primaryCertificate, IEnumerable<byte[]> additionalCertificates)
{
    var chain = new X509Chain();
    foreach (var cert in additionalCertificates.Select(x => new X509Certificate2(x)))
    {
        chain.ChainPolicy.ExtraStore.Add(cert);
    }

    // You can alter how the chain is built/validated.
    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreWrongUsage;

    // Do the preliminary validation.
    var primaryCert = new X509Certificate2(primaryCertificate);
    if (!chain.Build(primaryCert))
        return false;

    // Make sure we have the same number of elements.
    if (chain.ChainElements.Count != chain.ChainPolicy.ExtraStore.Count + 1)
        return false;

    // Make sure all the thumbprints of the CAs match up.
    // The first one should be 'primaryCert', leading up to the root CA.
    for (var i = 1; i < chain.ChainElements.Count; i++)
    {
        if (chain.ChainElements[i].Certificate.Thumbprint != chain.ChainPolicy.ExtraStore[i - 1].Thumbprint)
            return false;
    }

    return true;
}

由于我没有完整的CA证书链,因此无法测试此代码,所以最好通过调试和逐步执行代码进行排错。


谢谢。但我故意在“additionalCertificates”中放了不同的发行者证书,结果是“true”:( - Jacob
1
谢谢Jonathan。但是不起作用。看起来chain.Build正在验证证书的有效性。而不是证书路径。只要证书有效(尽管证书链/路径错误),结果就为true。指纹方法不起作用,因为所有指纹都不同(即使在正确的路径/链中)。 - Jacob
2
@Jacob,这就是为什么我手动验证指纹的原因,正如我所说,逐步执行代码的新部分。另外,在 if (chain.Build) 部分我打错了一个字母,请更新它。 - Jonathan Dickinson
primaryCertificate 是终端证书,而附加证书是从根证书颁发机构到 CA 的。 - Mehdi Golzadeh
chain.Build()永远不会返回false,因此并不是非常有用。即使我使用了错误的根证书 - 它仍然会返回true - Alexander Farber
1
@AlexanderFarber 这可能是因为系统中有一个证书已经被信任了。请参见 https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chainpolicy.trustmode?view=net-6.0,您现在可以限制使用哪些根证书。 - Charlieface

1

X509Chain 在没有根证书存储在受信任的 CA 存储区中的情况下,无法可靠地工作。

其他人会建议使用 bouncy castle。我想避免为此任务引入另一个库,所以我写了自己的代码。

RFC3280 第4.1节 所示,证书是一个 ASN1 编码结构,在其基本级别上仅由 3 个元素组成。

  1. "TBS"(待签名)证书
  2. 签名算法
  3. 和签名值
Certificate  ::=  SEQUENCE  {
     tbsCertificate TBSCertificate,
     signatureAlgorithm   AlgorithmIdentifier,
     signatureValue BIT STRING
}

C#实际上有一个方便的工具来解析ASN1,即System.Formats.Asn1.AsnDecoder

使用它,我们可以从证书中提取这3个元素以验证链。

第一步是提取证书签名,因为X509Certificate2类不公开此信息,而这对于证书验证是必要的。

提取签名值部分的示例代码:

public static byte[] Signature(
    this X509Certificate2 certificate,
    AsnEncodingRules encodingRules = AsnEncodingRules.BER)
{
    var signedData = certificate.RawDataMemory;
    AsnDecoder.ReadSequence(
        signedData.Span,
        encodingRules,
        out var offset,
        out var length,
        out _
    );

    var certificateSpan = signedData.Span[offset..(offset + length)];
    AsnDecoder.ReadSequence(
        certificateSpan,
        encodingRules,
        out var tbsOffset,
        out var tbsLength,
        out _
    );

    var offsetSpan = certificateSpan[(tbsOffset + tbsLength)..];
    AsnDecoder.ReadSequence(
        offsetSpan,
        encodingRules,
        out var algOffset,
        out var algLength,
        out _
    );

    return AsnDecoder.ReadBitString(
        offsetSpan[(algOffset + algLength)..],
        encodingRules,
        out _,
        out _
    );
}

下一步是提取TBS证书。这是已签名的原始数据。
提取TBS证书数据的示例代码:
public static ReadOnlySpan<byte> TbsCertificate(
    this X509Certificate2 certificate,
    AsnEncodingRules encodingRules = AsnEncodingRules.BER)
{
    var signedData = certificate.RawDataMemory;
    AsnDecoder.ReadSequence(
        signedData.Span,
        encodingRules,
        out var offset,
        out var length,
        out _
    );

    var certificateSpan = signedData.Span[offset..(offset + length)];
    AsnDecoder.ReadSequence(
        certificateSpan,
        encodingRules,
        out var tbsOffset,
        out var tbsLength,
        out _
    );

    // include ASN1 4 byte header to get WHOLE TBS Cert
    return certificateSpan.Slice(tbsOffset - 4, tbsLength + 4);
}

你可能会注意到,在提取TBS证书时,我需要在数据中包含ASN1头部,这是因为TBS证书的签名包括了这些数据(这让我有一段时间很烦恼)。
历史上第一次,微软没有阻碍我们的API设计,我们能够直接从X509Certificate2对象中获取签名算法。然后我们只需要决定要实现不同哈希算法的程度。
var signature = signed.Signature();
var tbs = signed.TbsCertificate();
var alg = signed.SignatureAlgorithm;

// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnap/a48b02b2-2a10-4eb0-bed4-1807a6d2f5ad
switch (alg)
{
    case { Value: var value } when value?.StartsWith("1.2.840.113549.1.1.") ?? false:
        return signedBy.GetRSAPublicKey()?.VerifyData(
            tbs,
            signature,
            value switch {
                "1.2.840.113549.1.1.11" => HashAlgorithmName.SHA256,
                "1.2.840.113549.1.1.12" => HashAlgorithmName.SHA384,
                "1.2.840.113549.1.1.13" => HashAlgorithmName.SHA512,
                _ => throw new UnsupportedSignatureAlgorithm(alg)
            },
            RSASignaturePadding.Pkcs1
        ) ?? false;
    case { Value: var value } when value?.StartsWith("1.2.840.10045.4.3.") ?? false:
        return signedBy.GetECDsaPublicKey()?.VerifyData(
            tbs,
            signature,
            value switch
            {
                "1.2.840.10045.4.3.2" => HashAlgorithmName.SHA256,
                "1.2.840.10045.4.3.3" => HashAlgorithmName.SHA384,
                "1.2.840.10045.4.3.4" => HashAlgorithmName.SHA512,
                _ => throw new UnsupportedSignatureAlgorithm(alg)
            },
            DSASignatureFormat.Rfc3279DerSequence
        ) ?? false;
    default: throw new UnsupportedSignatureAlgorithm(alg);
}

如上代码所示,https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnap/a48b02b2-2a10-4eb0-bed4-1807a6d2f5ad是一个很好的资源,用于查看算法和 OID 的映射。

另外,你应该知道有些文章声称对于椭圆曲线算法,微软期望使用 R,S 格式的密钥而不是 DER 格式的密钥。我试图将密钥转换为此格式,但最终失败了。我发现必须使用 DSASignatureFormat.Rfc3279DerSequence 参数。

除了链验证之外,还可以进行其他证书检查,例如“未过期”和“已过期”,或 CRL 和 OCSP 检查。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接