如何在 C# 中验证来自特定根 CA 的证书链



我有一个证书链,看起来像这样:root CA -> intermediate CA -> client certificate.如何验证收到的证书是否由"根 CA"显式创建?

验证整个链不是问题。这可以像这样完成:

X509Certificate2 rootCert = new X509Certificate2(rootCertFile);
X509Certificate2 intermediateCert = new X509Certificate2(intermediateCertFile);
X509Certificate2 clientCert = new X509Certificate2(clientCertFile);
chain.ChainPolicy.ExtraStore.Add(rootCert);
chain.ChainPolicy.ExtraStore.Add(intermediateCert);
if(chain.Build(clientCert))
{
// ... chain is valid
}

这里的问题是证书针对 (Windows) 证书存储进行验证,但我只想针对特定的根 CA 对其进行验证。

我还认为可以检查chain.ChainElements是否包含我预期的根 CA。但是,如果有人从不同的根 CA 向我发送有效链并只添加我预期的根 CA 怎么办?

证书链 API 检查每个元素是否对前面的元素进行了签名,因此不可能有人可以在末尾固定您的根 CA(前提是您没有使用带有 MD5 签名的 384 位 RSA 密钥,在这种情况下,他们可以伪造您的签名)。

您可以对任何您喜欢的额外检查进行编码,例如您知道您的链都不会超过 3 长度(尽管您可以在根 CA 的 X509 基本约束扩展中对其进行编码)。

if (!chain.Build(cert))
{
return false;
}
if (chain.ChainElements.Length > 3)
{
return false;
}
X509Certificate2 chainRoot = chain.ChainElements[chain.ChainElements.Length - 1].Certificate;
return chainRoot.Equals(root);

如果您更喜欢,可以return root.RawData.SequenceEquals(chainRoot.RawData);最后一行(确保它们具有相同的字节)。

一些注意事项:

  • 当你调用X509Chain.Build()时,每个X509Certificate2对象它通过X509ChainElement返回是一个新对象。您可能希望释放任何未返回的对象(可能是所有对象)。
  • 即使链。构建返回 false 它将填充 ChainElements 数组,以便您可以检查原因。
  • X509Chain 对象本身是一次性的,您可能希望释放它(您可能已经在代码段之外执行此操作)。
  • 释放链不会释放任何创建的证书,因为您可能已经持有对象引用。

对于计算机上受信任的 CA 存储中没有根证书的情况,X509Chain无法可靠地工作。

其他人会提倡使用充气城堡。我想避免为这项任务引入另一个库,所以我编写了自己的库。

如第 4.1 节RFC3280所述,证书是一种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 证书的签名包含此数据(这让我烦恼了一段时间)。

有史以来第一次,Microsoft 不会阻碍我们的 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 映射的良好资源。

您应该注意的另一件事是,有一些文章声称对于椭圆曲线算法,Microsoft 需要R,S格式的密钥而不是 DER 格式的密钥。我试图将密钥转换为这种格式,但最终不起作用。我发现有必要使用DSASignatureFormat.Rfc3279DerSequence参数。

除了链验证之外,还可以执行其他证书检查,例如"不之前"和"不之后",或 CRL 和 OCSP 检查。

相关内容

  • 没有找到相关文章

最新更新