证书扩展值包含 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 字符串类型解码字符串值。
出于测试目的,我的单元测试使用 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 字符串类型解码字符串值。