Search code examples
c#bouncycastle

Non-ASCII characters in SubjectAlternativeName


I am trying to create self-signed X509 certificates, using BouncyCastle.NET. It seems to work in general, but I am failing for internationalised domain names that contain non-ASCII characters.

The following is a compact example of what I am doing. The unit test case for "myDevice.abc.example.com" succeeds, but it fails for other cases that include non-ASCII characters (like "myDevice.äöü.example.com").

[TestCase("myDevice.abc.example.com")]
[TestCase("myDevice.äöü.example.com")]  // western european
[TestCase("myDevice.ařa.example.com")]  // eastern european
[TestCase("mydevice.aデa.example.com")] // katakana
[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);

    //var dnsString = new Org.BouncyCastle.Asn1.DerIA5String(fqdn, true);   //explicit validation would fail here
    //var dnsName = new GeneralName(dnsString, GeneralName.DnsName);
    var dnsName = new GeneralName(GeneralName.DnsName, fqdn);               //here I can create an GeneralName without validation failure
    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]);
    object actual = generalName[1];
    Assert.AreEqual(fqdn, actual);
    #endregion
}

The final assertion fails, special characters in the string retrieved from the certificate are replaced by '?'.

Is this something that should work, am I doing something obvious wrong?


Solution

  • Thanks to the comments by DJDaveMark and James K Polk I found the answer: The domain name must be encoded according to rfc 5280 before creating the GeneralName:

    var idn = new System.Globalization.IdnMapping();
    idn.UseStd3AsciiRules = true;
    idn.AllowUnassigned = false;
    string encodedFqdn = idn.GetAscii(fqdn);
    var dnsName = new GeneralName(GeneralName.DnsName, encodedFqdn);
    

    And obviously it must be decoded after reading from the certificate:

    var actualEncodedFqdn = generalName[1];
    var actual = idn.GetUnicode(actualEncodedFqdn.ToString());
    StringAssert.AreEqualIgnoringCase(fqdn, actual);