Search code examples
c#.net-corex509certificatebouncycastlex509

Certificate extension value contains 2 extra bytes (\u0004\u0002) octet encoding


for testing purposes my unittest generates a test certificate with custom extensions using BouncyCastle for .NET core.

Generate function

static internal class CertificateGenerator
{
    public static X509Certificate2 GenerateCertificate(string region)
    {
        var randomGenerator = new CryptoApiRandomGenerator();
        var random = new SecureRandom(randomGenerator);
        var certificateGenerator = new X509V3CertificateGenerator();
        var serialNumber =
            BigIntegers.CreateRandomInRange(
                BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
        certificateGenerator.SetSerialNumber(serialNumber);
        const string signatureAlgorithm = "SHA1WithRSA";
        certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
        var subjectDN = new X509Name("CN=FOOBAR");
        var issuerDN = subjectDN;
        certificateGenerator.SetIssuerDN(issuerDN);
        certificateGenerator.SetSubjectDN(subjectDN);
        var notBefore = DateTime.UtcNow.Date.AddHours(-24);
        var notAfter = notBefore.AddYears(1000);
        certificateGenerator.SetNotBefore(notBefore);
        certificateGenerator.SetNotAfter(notAfter);
      
        var fakeOid = "1.3.6.1.1.5.6.100.345434.345";
        if (region != null)
        {
            certificateGenerator.AddExtension(new DerObjectIdentifier(fakeOid), false, Encoding.ASCII.GetBytes(region));
        }

        const int strength = 4096;
        var keyGenerationParameters = new KeyGenerationParameters(random, strength);
        var keyPairGenerator = new RsaKeyPairGenerator();
        keyPairGenerator.Init(keyGenerationParameters);
        var subjectKeyPair = keyPairGenerator.GenerateKeyPair();

        certificateGenerator.SetPublicKey(subjectKeyPair.Public);

        var issuerKeyPair = subjectKeyPair;
        var certificate = certificateGenerator.Generate(issuerKeyPair.Private, random);

        var store = new Pkcs12Store();
        string friendlyName = certificate.SubjectDN.ToString();
        var certificateEntry = new X509CertificateEntry(certificate);
        store.SetCertificateEntry(friendlyName, certificateEntry);

        store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });

        string password = "password";
        var stream = new MemoryStream();
        store.Save(stream, password.ToCharArray(), random);


        byte[] pfx = Pkcs12Utilities.ConvertToDefiniteLength(stream.ToArray(), password.ToCharArray());

        var convertedCertificate =
            new X509Certificate2(
                pfx, password,
                X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);

        return convertedCertificate;
    }
}

Reader

public class CertificateExtensionReader
{
    private readonly ILogger logger;

    public CertificateExtensionReader(ILogger logger)
    {
        this.logger = logger;
    }

    public CertificateExtensionValues ReadExtensionValues(byte[] certificate)
    {
        var x509Certificate2 = new X509Certificate2(certificate);
        var region = GetCustomExtensionValue(x509Certificate2.Extensions, new Oid("1.3.6.1.1.5.6.100.345434.345"));

        return new CertificateExtensionValues { Region = region };
    }

    private string GetCustomExtensionValue(X509ExtensionCollection x509Extensions, Oid oId)
    {
        var extension = x509Extensions[oId.Value];

        if(extension == null)
            throw new CertificateExtensionValidationException($"The client certificate does not contain the expected extensions '{oId.FriendlyName}' with OID {oId.Value}.");

        if (extension.RawData == null)
            throw new CertificateExtensionValidationException($"Device client certificate does not a value for the '{oId.FriendlyName}' extension with OID {oId.Value}");

        var customExtensionValue = Encoding.UTF8.GetString(extension.RawData).Trim();
        logger.LogInformation($"Custom Extension value for the '{oId.FriendlyName}' extension with OID {oId.Value}: '{customExtensionValue}'");
        return customExtensionValue;
    }
}

public class CertificateExtensionValues
{
    public string Region { get; set; }
}

Test

[TestFixture]
public class CertificateExtensionReaderFixture
{
    private ILogger logger = new NullLogger<CertificateExtensionReaderFixture>();
    private CertificateExtensionReader reader;

    [SetUp]
    public void Setup()
    {
        reader = new CertificateExtensionReader(logger);
    }

    [Test]
    public void ShouldReadExtensionValues()
    {
        var certificate = CertificateGenerator.GenerateCertificate("r1").Export(X509ContentType.Pfx);

        var values = reader.ReadExtensionValues(certificate);

        values.Region.Should().Be("r1");
    }
}

Expected values.Region to be "r1" with a length of 2, but "\u0004\u0002r1" has a length of 4, differs near "\u0004\u0002r" (index 0).

So BouncyCastle added two extra bytes \u0004\u0002 (End of transmission, begin of text) for the extension value.

I saved the certificate to a file and dumped it via certutil -dump -v test.pfx

certutil dump

What am I doing wrong? Is it the generation of the certificate? Or is is how I read the values? Are all extension values encoded this way? I was expecting only the string bytes. I could not find something in the spec.


Solution

  • What am I doing wrong?

    you are creating extension wrong.

    certificateGenerator.AddExtension(new DerObjectIdentifier(fakeOid), false, Encoding.ASCII.GetBytes(region));
    

    Last parameter is incorrect. It must contain any valid ASN.1 type. Since parameter value is invalid ASN.1 type, BC assumes it is just a random/arbitrary octet string and implicitly encodes raw value into ASN.1 OCTET_STRING type. If it is supposed to be a text string, then use any of applicable ASN.1 string types that fits you requirements and char set.

    And you have to update your reader to expect ASN.1 string type you chose for encoding and then decode string value from ASN.1 string type.