证书扩展值包含 2 个额外字节 (\u0004\u0002) 八位字节编码

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

出于测试目的,我的单元测试使用 BouncyCastle for .NET core 生成了带有自定义扩展的测试证书。

生成函数

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; }
}

测试

[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).

因此 BouncyCastle 添加了两个额外的字节 \u0004\u0002(传输结束,文本开始)作为扩展值。

我将证书保存到文件中并通过 certutil -dump -v test.pfx

将其转储

我做错了什么?是证书的生成吗?或者是我如何阅读价值观?所有的扩展值都是这样编码的吗?我只期待字符串字节。我在规范中找不到任何内容。

What am I doing wrong?

您创建的扩展程序有误。

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

最后一个参数不正确。它必须包含任何有效的 ASN.1 类型。由于参数值是无效的 ASN.1 类型,BC 假定它只是一个 random/arbitrary 八位字节字符串,并将原始值隐式编码为 ASN.1 OCTET_STRING 类型。如果它应该是文本字符串,则使用适合您要求和字符集的任何适用的 ASN.1 字符串类型。

并且您必须更新 reader 以期望您选择用于编码的 ASN.1 字符串类型,然后从 ASN.1 字符串类型解码字符串值。