如何使用 BouncyCastle X509V3CertificateGenerator 设置 upnName(用户主体名称)

How can the upnName (user principal name) be set with BouncyCastle X509V3CertificateGenerator

我正在为通过

从给定证书收集用户主体名称的应用程序中预先存在/运行的方法编写一些单元测试
var upnName = currentUserCert.GetNameInfo(X509NameType.UpnName, false);

这在 运行 时间有效,但是,到目前为止,我无法从 BouncyCastle X509V3CertificateGenerator 生成的证书中检索此值。虽然我可以完成大部分工作,但我无法生成包含 upnName 的证书。我只是不确定如何设置 user principal 以便在测试时可以通过 GetNameInfo 检索它。

测试证书本身就是这样生成的

private X509Certificate2 GenerateCert(KeyPurposeID keyPurposeId, string subjectPrefix = "OID.1.2.3.4=", string validCertOriginFragment = "OU=SOME ORG")
{
    const int keyStrength = 2048;
    const string subject = "98876543210123";

    // Generating Random Numbers
    var randomGenerator = new CryptoApiRandomGenerator();
    var random = new SecureRandom(randomGenerator);
    var kpgen = new RsaKeyPairGenerator();

    kpgen.Init(new KeyGenerationParameters(new SecureRandom(new CryptoApiRandomGenerator()), keyStrength));

    var certificateGenerator = new X509V3CertificateGenerator();

    var certName = new X509Name($"{subjectPrefix}{subject} + CN=JAMES BOND (Affiliate), {validCertOriginFragment}, O=SOME ORG, C=US");
    var issuer = new X509Name("OU=CERT AUTH CA, OU=Certification Authorities, O=MYCERTAUTH, C=US");
    var serialNo = BigInteger.ProbablePrime(120, new Random());

    certificateGenerator.SetSerialNumber(serialNo);
    certificateGenerator.SetSubjectDN(certName);
    certificateGenerator.SetIssuerDN(issuer);
    certificateGenerator.SetNotAfter(DateTime.Now.AddYears(50));
    certificateGenerator.SetNotBefore(DateTime.Now);

    // TODO setup upn name

    var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
    var keyPairGenerator = new RsaKeyPairGenerator();
    keyPairGenerator.Init(keyGenerationParameters);
    var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
    certificateGenerator.SetPublicKey(subjectKeyPair.Public);

    if (keyPurposeId != null)
    {
        certificateGenerator.AddExtension(
            X509Extensions.ExtendedKeyUsage.Id,
            false,
            new ExtendedKeyUsage(keyPurposeId));
    }

    // Generating the Certificate
    var issuerKeyPair = subjectKeyPair;
    ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", issuerKeyPair.Private, random);
    // selfsign certificate
    var certificate = certificateGenerator.Generate(signatureFactory);
    var x509 = new X509Certificate2(certificate.GetEncoded());

    return x509;
}

根据@peop 的建议,将以下内容添加到设置 upn 名称的 TODO 中

certificateGenerator.AddExtension("2.5.29.17", true,
                                new GeneralNames(
                                    new GeneralName(GeneralName.OtherName,
                                                    new DerSequence(new DerObjectIdentifier("1.3.6.1.4.1.311.20.2.3"), new DerUtf8String($"{subject}@SOMEWHERE.COM")))
                                    ));

让我更接近了,我认为,在调查证书时我有一个很好的额外扩展,但是预期的信息仍然没有通过 GetNameInfo 方法公开。为了使神奇的 Microsoft GetNameInfo 方法亮起并 return 预期值,证书是否必须发生更多事情?

如何通过 BouncyCastle X509V3CertificateGenerator 创建证书以便我可以检索 upnName 以进行测试?

根据 this page UPN 应该是 SubjectAlternativeName (SAN) 扩展的一部分。

Subject Alternative Name = Other Name: Principal Name= (UPN). For example: UPN = user1@name.com The UPN OtherName OID is : "1.3.6.1.4.1.311.20.2.3" The UPN OtherName value: Must be ASN1-encoded UTF8 string

可以找到使用 X509V3CertificateGenerator 生成 SAN 扩展的示例 here

// example of adding email address to SAN
certGen.AddExtension("2.5.29.17", true,
    new GeneralNames(new GeneralName(GeneralName.Rfc822Name, "test@test.test")));

非常接近,但我们需要 GeneralName.OtherName,它定义为 here

所以我的伪代码(用记事本编写)应该是这样的:

certificateGenerator.AddExtension("2.5.29.17", true,
  new GeneralNames(
      new GeneralName(GeneralName.OtherName,
        new DerSequence(new DerObjectIdentifier("1.3.6.1.4.1.311.20.2.3"), 
        new DerUtf8String(upnName)))
      ));

希望我的 ASN.1 结构正确 - Sequence->(oid, utf8string)。最好的方法是解析现有证书,即 ASN.1 editor 以检查扩展的 ASN.1 结构。

Microsoft 提供 Guidelines for enabling smart card logon with third-party certification authorities。在该文档中列举了证书的特定格式要求:

  • The CRL Distribution Point (CDP) location (where CRL is the Certification Revocation List) must be populated, online, and available. For example:
    [1]CRL Distribution Point
    Distribution Point
    Name:
    Full Name: URL=http://server1.name.com/CertEnroll/caname.crl

  • Key Usage = Digital Signature

  • Basic Constraints [Subject Type=End Entity, Path Length Constraint=None (Optional)

  • Enhanced Key Usage =

    • Client Authentication (1.3.6.1.5.5.7.3.2)
      (The client authentication OID) is only required if a certificate is used for SSL authentication.)
    • Smart Card Logon (1.3.6.1.4.1.311.20.2.2)
  • Subject Alternative Name = Other Name: Principal Name= (UPN). For example:
    UPN = user1@name.com
    The UPN OtherName OID is : "1.3.6.1.4.1.311.20.2.3"
    The UPN OtherName value: Must be ASN1-encoded UTF8 string

  • Subject = Distinguished name of user. This field is a mandatory extension, but the population of this field is optional.

鉴于格式要求和您的证书生成代码(包括您根据 添加的 UPN),您需要修改向证书添加扩展的部分:

if (keyPurposeId != null)
{
    certificateGenerator.AddExtension(
        X509Extensions.ExtendedKeyUsage.Id,
        false,
        new ExtendedKeyUsage(keyPurposeId));

    // new extension not present in your question's code
    certificateGenerator.AddExtension(
        "1.3.6.1.5.5.7.3.2",
        false,
        new ExtendedKeyUsage(KeyPurposeID.IdKPClientAuth));
}

这样做应该会创建一个证书,可以用作 GetNameInfo(X509NameType.UpnName, false);.

X509Certificate2 实例

以下对我有用,并生成了一个带有主题备用名称属性的证书,该属性与用于 user/client 身份验证的 Microsoft 证书服务生成的证书格式相同。也就是说,在 Windows 中查看 .cer 文件的 Subject Alternative Name 属性时,您会看到:

    Other Name:
        Principal Name=john.doe@contoso.com
    RFC822 Name=john.doe@contoso.com
certificateGenerator.AddExtension(X509Extensions.SubjectAlternativeName.Id, false, new DerSequence(
            new GeneralName(GeneralName.OtherName,
                new DerSequence(new DerObjectIdentifier("1.3.6.1.4.1.311.20.2.3"),
                    new DerTaggedObject(0, new DerUtf8String("john.doe@contoso.com")))),
            new GeneralName(GeneralName.Rfc822Name, "john.doe@costoso.com")));

关键是将 UPN 字符串包装在 DerTaggedObject 中。正如 pepo, I used the ASN.1 Editor to view an existing certificate. It had a bunch of Context Specific nodes around these values. So doing research on that, I found Tutorial for ASN1 DER Primitive Encoder 所建议的那样,这使我进入了 DerTaggedObject。从那里开始,我开始尝试一些代码,直到我生成的证书的输出与 Microsoft 证书服务创建的证书的输出相匹配。

另请注意,Subject Alternative Name 的扩展未标记为 criticalAddExtension 方法的第二个参数是 false)。这再次匹配 Microsoft 证书服务生成的内容。