Search code examples
c#.net-standardx509certificate2

X509FindType.FindBySubjectDistinguishedName not finding my certificate?


I am making a self-signed X509 certificate as follows:

public X509Certificate2 GenerateSelfSignedCertificate(Guid customerId)
{
    var distinguishedName = GetDistinguishedName(customerId);
    var rsa = RSA.Create(KeySize);
    var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithm, Padding);
    request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
    request.CertificateExtensions.Add(new X509KeyUsageExtension(
        X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.DataEncipherment |
        X509KeyUsageFlags.NonRepudiation, false));

    var cert = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.Add(CertificateLifespan));
    return cert;
}

I then use this method to generate the cert and store it using the following method:

public void StoreCertificate(X509Certificate2 certificate)
{
    using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
    store.Open(OpenFlags.ReadWrite);
    store.Add(certificate);
    store.Close();
}

Next, I try to fetch the certificate from the certificate store using the following method:

public X509Certificate2 FetchPrivateCertificate(Guid customerId)
{
    var distinguishedName = GetDistinguishedName(customerId);
    using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
    store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
    var certs = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, distinguishedName, false);
    store.Close();
    var privateCert = certs.OfType<X509Certificate2>().FirstOrDefault(x => x.HasPrivateKey);
    return privateCert;
}

...using the same customerId, this result is always null. It's not even getting to check if the cert has a private key, the cert collection is always empty. The distinguished name is generated exactly the same way. The certificate is definitely in the cert store.

What am I missing?

UPDATE

Per request, I'm adding how the Distinguished Name is generated:

private string GetDistinguishedName(Guid customerId)
{
    var domain = IPGlobalProperties.GetIPGlobalProperties().DomainName;
    var domainDistinguishedName = string.Join(",DC=", domain.Split("."));
    var distinguishedName = $"DC={Environment.MachineName},DC={domainDistinguishedName},O={customerId}";

    return distinguishedName;
}

Solution

  • The problem (as pointed in comments) is that your GetDistinguishedName returns unformatted x.500 string which is sufficient to build an X.500 name, but not sufficient for searching, because search function makes ordinal and case-insensitive comparison of input string and *formatted* X.500 string. That is, you can use these string formats to build the X.500 names:

    CN=my common name,O=example LLC
    CN = my common name,O = example LLC
    CN= my common name, O =example LLC
    <other variations>
    

    CryptoAPI uses X.500 formatter to format any valid X.500 string to a consistent-looking string, e.g. capitalize RDN attribute names, remove spaces around = character, add space after RDN delimiter and even more, the function may re-order RDN attributes in the string. And when seraching for certs by X.500 name, you have to pass formatted/sanitized X.500 name to search function. The easiest way to do so is to update your GetDistinguishedName function as follows:

    private string GetDistinguishedName(Guid customerId)
    {
        var domain = IPGlobalProperties.GetIPGlobalProperties().DomainName;
        var domainDistinguishedName = string.Join(",DC=", domain.Split("."));
        var distinguishedName = $"DC={Environment.MachineName},DC={domainDistinguishedName},O={customerId}";
    
        return new X500DistinguishedName(distinguishedName).Name;
    }
    

    i.e. force string formatting before returning it.

    p.s. DC={Environment.MachineName} uses incorrect RDN attribute. DC stands for domainComponent. Machine name isn't a domain component by any mean. Use CN (common name) attribute to store machine name. And O stands for Organization. I suspect it is misused too, because customerId barely holds org name (just an assumption). You should use proper RDN types in subject/issuer strings.