使用BouncyCastle.NET生成含非ASCII字符SAN的X509证书断言失败
解决BouncyCastle.NET处理IDN域名时特殊字符丢失的问题
这个问题的核心在于X.509证书的DNS主题备用名称(SAN)要求使用IA5String编码,而IA5String仅支持ASCII字符——直接传入带非ASCII的国际化域名(IDN)时,BouncyCastle会自动把非ASCII字符替换成?,这就是你断言失败的原因。
为什么会出现这个问题?
当你调用new GeneralName(GeneralName.DnsName, fqdn)时,BouncyCastle默认会把传入的字符串当作IA5String处理。IA5String的字符范围仅限ASCII,任何超出范围的字符都会被替换为?,所以你从证书里读回的字符串才会和原域名不一致。你之前尝试用DerIA5String(fqdn, true)显式验证失败,也是因为原域名包含非ASCII字符,违反了IA5String的编码规则。
正确的处理方式(符合标准)
根据RFC 5280和IDN规范,国际化域名需要先转换为Punycode(ASCII兼容编码,ACE),再存入证书的DNS SAN中。.NET提供了System.Globalization.IdnMapping类来处理这种转换,具体步骤如下:
- 将原IDN域名转换为Punycode
- 使用转换后的Punycode创建
GeneralName - 验证时,把证书中读取到的Punycode转换回Unicode域名再做断言
修改后的代码示例:
using System.Globalization; [TestCase("myDevice.abc.example.com")] [TestCase("myDevice.äöü.example.com")] // 西欧字符 [TestCase("myDevice.ařa.example.com")] // 东欧字符 [TestCase("mydevice.aデa.example.com")] // 片假名 [Test] public void IdnTest(string fqdn) { #region Preparation ECKeyPairGenerator kpgen = new ECKeyPairGenerator(); kpgen.Init(new KeyGenerationParameters(new SecureRandom(new CryptoApiRandomGenerator()), Constants.SelectedRootKeySize)); var caKeyPair = kpgen.GenerateKeyPair(); var certKeyPair = kpgen.GenerateKeyPair(); X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator(); certGenerator.SetSerialNumber(Org.BouncyCastle.Math.BigInteger.ProbablePrime(120, new Random())); IList oids = new ArrayList() { X509Name.OU }; IList values = new ArrayList() { "Test" }; certGenerator.SetIssuerDN(new X509Name(oids, values)); certGenerator.SetNotBefore(DateTime.Now.Date); certGenerator.SetNotAfter(DateTime.Now + TimeSpan.FromDays(365)); certGenerator.SetPublicKey(certKeyPair.Public); // 关键:将IDN域名转换为Punycode var idnMapping = new IdnMapping(); string punycodeFqdn = idnMapping.GetAscii(fqdn); var dnsName = new GeneralName(GeneralName.DnsName, punycodeFqdn); GeneralNames subjectAltName = new GeneralNames(dnsName); certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, true, subjectAltName); ISignatureFactory signatureFactory = new Asn1SignatureFactory("sha256WithECDSA", caKeyPair.Private, new SecureRandom(new CryptoApiRandomGenerator())); Org.BouncyCastle.X509.X509Certificate x509Certificate = certGenerator.Generate(signatureFactory); #endregion #region Verification var san = x509Certificate.GetSubjectAlternativeNames() as ArrayList; Assert.AreEqual(1, san.Count); var generalName = san[0] as System.Collections.ArrayList; Assert.AreEqual(GeneralName.DnsName, generalName[0]); // 验证时将Punycode转换回Unicode域名 string actualPunycode = generalName[1].ToString(); string actualFqdn = idnMapping.GetUnicode(actualPunycode); Assert.AreEqual(fqdn, actualFqdn); #endregion }
额外说明
如果你尝试直接用UTF8编码存储域名(比如用DerUtf8String构造GeneralName),虽然技术上可行,但不符合X.509标准,会导致很多TLS客户端(比如浏览器、操作系统)无法正确识别证书中的域名,因此强烈不建议这么做。
内容的提问来源于stack exchange,提问作者pniederh
相关产品推荐
相关产品推荐

