Search code examples
c#unit-testingcertificatebouncycastlex509

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


I'm writing some unit tests for a pre-existing / functioning method in an application that gathers the user principal name from a given certificate via

var upnName = currentUserCert.GetNameInfo(X509NameType.UpnName, false);

This works at run time however, I'm so far unable to retrieve this value from a BouncyCastle X509V3CertificateGenerator generated certificate. While I can get most of the way there I've been unable to generate a certificate that contains a upnName. I'm simply unsure how to set the user principal such that it may be retrieved via GetNameInfo when testing.

The test certificate itself is being generated as such

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

As per the suggestion by @peop adding the following to the TODO of setup upn name

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")))
                                    ));

Has gotten me closer, I think, upon investigating the certificate I have an extra extension which is good, however the expected information is still not exposed via the GetNameInfo method. Is there something more that must happen to the certificate in order for the magical Microsoft GetNameInfo method to light up and return the expected value?

How do I create a certificate via the BouncyCastle X509V3CertificateGenerator such that I can retrieve the upnName for the sake of testing?


Solution

  • Microsoft provides Guidelines for enabling smart card logon with third-party certification authorities. In that document the specific format requirements for the certificate are enumerated:

    • 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 = [email protected]
      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.

    Given the format requirements and your certificate generation code (including the UPN you've added based on pepo's answer) you need to modify the section where you are adding extensions to the cert:

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

    Doing this should create a cert that can be used as an X509Certificate2 instance to GetNameInfo(X509NameType.UpnName, false);.