手动验证TLS证书链签名失败,求C#实现问题排查
手动验证TLS证书签名失败排查
问题描述
为学习目的,尝试用C#手动验证TLS证书链(仅验证签名),梳理的验证步骤如下:
- 计算证书的哈希值;
- 读取证书的签名哈希值;
- 使用父证书公钥调用
RSA.Verify()方法进行验证。
编写的代码如下:
var myCert = new X509Certificate2(@"Z:\springer.com.crt"); var parentCert = new X509Certificate2(@"Z:\R3.crt"); var certHash = myCert.GetCertHash(HashAlgorithmName.SHA256); var certSignature = new byte[] { 0xAA, 0xF7, 0x2A, 0x84, 0x95, 0x17, 0x2E, 0xCD, 0x58, 0x84, 0x66, 0x9A, 0xC0, 0x0B, 0x3E, 0x13, 0x0C, 0x94, 0x34, 0x35, 0x31, 0x85, 0xA3, 0x43, 0xBC, 0xDA, 0xFC, 0x00, 0x3F, 0xB3, 0x6D, 0x9E, 0x4A, 0xAB, 0xEE, 0xDB, 0x34, 0xCB, 0xCE, 0x8F, 0xD2, 0x04, 0x73, 0x4B, 0xE7, 0x11, 0x5E, 0x9E, 0xDC, 0x6B, 0x8A, 0xDB, 0xDD, 0xB2, 0x6B, 0x1A, 0x7C, 0xE3, 0xFA, 0x97, 0xCC, 0xDF, 0x03, 0xAE, 0xFC, 0x41, 0xDC, 0x5D, 0x6E, 0x70, 0x71, 0x2D, 0x67, 0x96, 0x0D, 0x94, 0xC0, 0x41, 0xD9, 0xD0, 0x4E, 0xD7, 0x88, 0x99, 0xED, 0xDB, 0x84, 0x9D, 0x35, 0x73, 0xB0, 0x4B, 0x0A, 0x28, 0x19, 0xAE, 0x62, 0xD4, 0xEE, 0x17, 0xF9, 0x83, 0xFA, 0xDF, 0x56, 0xBE, 0xBB, 0xE9, 0x8F, 0x78, 0x85, 0x11, 0x57, 0x0C, 0xC4, 0x46, 0xEC, 0xDA, 0xE2, 0xD3, 0x14, 0x86, 0xEA, 0x65, 0xC0, 0x19, 0xDA, 0x9B, 0x8D, 0x21, 0x7D, 0x34, 0xBA, 0x0E, 0xD4, 0xC5, 0xD2, 0x20, 0x32, 0x15, 0xAA, 0x2C, 0x3B, 0x72, 0x95, 0x8E, 0xA7, 0x72, 0x26, 0x99, 0xA0, 0x5F, 0x43, 0xAC, 0xF9, 0x42, 0x91, 0x35, 0x0C, 0xB0, 0x0E, 0xE9, 0xF4, 0xBC, 0x25, 0x64, 0x59, 0x98, 0x5E, 0x34, 0x65, 0x9B, 0x28, 0x28, 0x3D, 0x88, 0x23, 0x25, 0x00, 0x97, 0x38, 0xE1, 0x76, 0xBC, 0x74, 0xB9, 0xFF, 0xBD, 0x17, 0x60, 0x1F, 0x07, 0x05, 0xF0, 0xCF, 0xB4, 0x5E, 0x13, 0x8F, 0xBE, 0x48, 0x52, 0xF2, 0xD7, 0xBA, 0xEC, 0x44, 0xDC, 0x93, 0x7D, 0x16, 0x82, 0x8F, 0xE5, 0xC3, 0x55, 0x31, 0x99, 0x0A, 0xE1, 0xB1, 0xC4, 0xE4, 0xCE, 0x31, 0xA5, 0x91, 0xFD, 0x71, 0xF6, 0xF7, 0x01, 0x6D, 0x30, 0x2E, 0xFA, 0x30, 0x41, 0x11, 0xBB, 0x96, 0x4B, 0xA1, 0xF0, 0x10, 0x71, 0x60, 0x07, 0x5B, 0xD2, 0x30, 0xA0, 0xC2, 0x29, 0x0E, 0x71 }; var result = parentCert.GetRSAPublicKey().VerifyHash(certHash, certSignature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
但始终无法让result返回True,请问哪里存在问题?
备注:使用的TLS证书来自springer.com;由于.NET原生X509Certificate类没有提取签名的方法,且引入BouncyCastle过于冗余,故硬编码了证书签名值。
问题根源及解决方案
1. 错误的哈希计算对象
GetCertHash()返回的是整个证书的指纹哈希,而X.509证书的签名是对证书的TBS(To Be Signed)部分进行签名的,这部分是证书中除了签名算法和签名值之外的核心内容。直接用证书指纹哈希验证签名,对象完全错误,这是导致验证失败的核心原因。
2. 硬编码签名的格式/字节序问题
X.509证书的签名值是ASN.1 DER编码的,且存储时可能存在字节序反转或格式偏差,硬编码的签名值很可能没有匹配正确的验证格式,导致验证不通过。
3. 无需硬编码签名:用.NET原生API提取TBS和签名
可以通过解析证书的原始ASN.1数据,直接提取TBS部分和签名值,无需依赖第三方库。以下是修正后的代码:
using System.Security.Cryptography; using System.Security.Cryptography.Asn1; var myCert = new X509Certificate2(@"Z:\springer.com.crt"); var parentCert = new X509Certificate2(@"Z:\R3.crt"); // 解析证书原始ASN.1数据,提取TBS部分和签名值 using (var asnReader = new AsnReader(myCert.RawData, AsnEncodingRules.DER)) { // 证书结构:SEQUENCE { tbsCertificate SEQUENCE, signatureAlgorithm AlgorithmIdentifier, signatureValue BIT STRING } var certSequence = asnReader.ReadSequence(); // 读取TBS证书部分(待签名的核心内容) var tbsCertificate = certSequence.ReadEncodedValue(); // 跳过签名算法字段(已明确使用SHA256,无需解析) certSequence.ReadEncodedValue(); // 读取签名值:BIT STRING的第一个字节是未使用的比特数,需跳过 var signatureBitString = certSequence.ReadBitString(out int unusedBits); var certSignature = signatureBitString.Skip(1).ToArray(); // 计算TBS部分的SHA256哈希 using (var sha256 = SHA256.Create()) { var tbsHash = sha256.ComputeHash(tbsCertificate); // 使用父证书公钥验证签名 var result = parentCert.GetRSAPublicKey() .VerifyHash(tbsHash, certSignature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); Console.WriteLine(result ? "验证成功" : "验证失败"); } }
关键说明
- X.509证书遵循固定的ASN.1结构,必须按顺序读取TBS、签名算法、签名值三个部分。
- 签名值存储为BIT STRING类型,其第一个字节表示未使用的比特数,验证时必须跳过该字节才能得到正确的签名数据。
- 只有TBS部分的哈希,才是签名对应的原始哈希值,与签名验证的逻辑完全匹配。
内容的提问来源于stack exchange,提问作者Ryan B.
相关产品推荐
相关产品推荐

