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?
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 stringSubject = 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);
.