X509Chain
在没有根证书存储在受信任的 CA 存储区中的情况下,无法可靠地工作。
其他人会建议使用 bouncy castle。我想避免为此任务引入另一个库,所以我写了自己的代码。
如 RFC3280 第4.1节 所示,证书是一个 ASN1
编码结构,在其基本级别上仅由 3 个元素组成。
- "TBS"(待签名)证书
- 签名算法
- 和签名值
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 _
);
return certificateSpan.Slice(tbsOffset - 4, tbsLength + 4);
}
你可能会注意到,在提取TBS证书时,我需要在数据中包含ASN1头部,这是因为TBS证书的签名包括了这些数据(这让我有一段时间很烦恼)。
历史上第一次,微软没有阻碍我们的API设计,我们能够直接从X509Certificate2对象中获取
签名算法。然后我们只需要决定要实现不同哈希算法的程度。
var signature = signed.Signature();
var tbs = signed.TbsCertificate();
var alg = signed.SignatureAlgorithm;
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 检查。
if (chain.Build)
部分我打错了一个字母,请更新它。 - Jonathan Dickinsonchain.Build()
永远不会返回false
,因此并不是非常有用。即使我使用了错误的根证书 - 它仍然会返回true
。 - Alexander Farber