Create and add certificate to certificate store in .NET 5.

3 minute read

This post talks about how to issue certificate with the private key and with the persist storage flag before adding it to the certificate store.

.NET 5 supports for creating a self-signed certificate and for issuing a certificate. Before the issued certificate is added to the certificate store, extra steps are needed to get the desired private key and storage flag settings.

First, let’s create a certificate with which we can issue other certificates. In the sample codes, we create a self-signed certificate by using CertificateRequest. You can also load the existing certificate (with private key).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/// <summary>
/// Create a self-signed certificate.
/// </summary>
/// <param name="keySize">The RAS key size in bits.</param>
/// <param name="commonName">The certificate common name.</param>
/// <param name="notBefore">The certificate starting time.</param>
/// <param name="notAfter">The certificate expiration time.</param>
/// <returns>The X509Certificate2 certificate.</returns>
public static X509Certificate2 CreateSelfSignedCert(int keySize, string commonName,
    System.DateTimeOffset notBefore, System.DateTimeOffset notAfter)
{
    using var rsa = RSA.Create(keySize);
    var request = new CertificateRequest(
        $"CN={commonName}",
        rsa,
        HashAlgorithmName.SHA512,
        RSASignaturePadding.Pkcs1);

    request.CertificateExtensions.Add(new X509BasicConstraintsExtension(
        true/*certificateAuthority*/,
        false/*hasPathLengthConstraint*/,
        0/*pathLengthConstraint*/,
        true/*critical*/));

    request.CertificateExtensions.Add(new X509KeyUsageExtension(
        X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign/*keyUsages*/,
        false/*critical*/));

    request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(
        request.PublicKey/*subjectKeyIdentifier*/,
        false/*critical*/));

    var subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder();
    subjectAlternativeNameBuilder.AddDnsName("test.com");
    subjectAlternativeNameBuilder.AddIpAddress(IPAddress.Loopback);
    request.CertificateExtensions.Add(subjectAlternativeNameBuilder.Build());

    return request.CreateSelfSigned(notBefore, notAfter);
}

Then we use this self-signed certificate to issue a leaf certificate in the following sample codes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/// <summary>
/// Issue a signed certificate by the parent cert.
/// </summary>
/// <param name="parentCert">The certificate used to sign this certificate.</param>
/// <param name="keySize">The RAS key size in bits.</param>
/// <param name="commonName">The certificate common name.</param>
/// <param name="flags">The certificate key usage flags.</param>
/// <param name="oidCollection">The enhanced key usages.</param>
/// <param name="notBefore">The certificate starting time.</param>
/// <param name="notAfter">The certificate expiration time.</param>
/// <param name="includePrivateKey">True to include the private key in the returned object.</param>
/// <returns>The X509Certificate2 certificate.</returns>
public static X509Certificate2 IssueSignedCert(X509Certificate2 parentCert, int keySize, string commonName,
    X509KeyUsageFlags flags, OidCollection oidCollection,
    System.DateTimeOffset notBefore, System.DateTimeOffset notAfter,
    bool includePrivateKey)
{
    using var rsa = RSA.Create(keySize);
    var request = new CertificateRequest(
        $"CN={commonName}",
        rsa,
        HashAlgorithmName.SHA512,
        RSASignaturePadding.Pkcs1);

    request.CertificateExtensions.Add(new X509BasicConstraintsExtension(
        false/*certificateAuthority*/,
        false/*hasPathLengthConstraint*/,
        0/*pathLengthConstraint*/,
        false/*critical*/));

    request.CertificateExtensions.Add(new X509KeyUsageExtension(
        flags/*keyUsages*/,
        false/*critical*/));

    request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(
        oidCollection/*oidCollection*/,
        true/*critical*/));

    request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(
        request.PublicKey/*subjectKeyIdentifier*/,
        false/*critical*/));

    var subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder();
    subjectAlternativeNameBuilder.AddDnsName("test.com");
    subjectAlternativeNameBuilder.AddIpAddress(IPAddress.Loopback);
    request.CertificateExtensions.Add(subjectAlternativeNameBuilder.Build());

    var serialNumber = new byte[SerialNumberSizeInBytes];
    RandomNumberGenerator.Fill(serialNumber);
    var cert = request.Create(parentCert, notBefore, notAfter, serialNumber);
    if (!includePrivateKey)
    {
        return cert;
    }

    var certWithPrivateKey = cert.CopyWithPrivateKey(rsa);
    cert.Dispose();
    return certWithPrivateKey;
}

Watch out! CertificateRequest.Create method returns a X509Certificate2 with HasPrivateKey = false. To get the private key, X509Certificate2.CopyWithPrivateKey method is used. If X509Certificate2.CopyWithPrivateKey is not used, the X509Certificate2 object does not have the private key.

At this point, the X509Certificate2 object still has a ephemeral private key. .NET so far does not let you set the private key storage flag yet. We need to get a new X509Certificate2 object with the correct private key storage flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// Get a certificate with the target key storage flags based on the original cert.
/// </summary>
/// <param name="cert">The original certificate.</param>
/// <param name="flags">The target key storage flags.</param>
/// <returns>The new  X509Certificate2 certificate.</returns>
public static X509Certificate2 GetCertWithStorageFlags(X509Certificate2 cert, X509KeyStorageFlags flags)
{
    return new X509Certificate2(
        cert.Export(X509ContentType.Pkcs12),
        string.Empty, flags
    );
}

Combining all of the above together, the following sample codes issue a certificate with the private key and with the PersisKeySet storage flag for TCP TLS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var notBefore = DateTimeOffset.UtcNow.AddDays(-45);
var notAfter = DateTimeOffset.UtcNow.AddDays(365);
// Create a self-signed certificate.
using var rootCert = CertificateOperations.CreateSelfSignedCert(
    CertificateOperations.KeySizeInBits, "A test root",
    notBefore, notAfter);
// Use the self-signed certificate to issue a TCP TLS certificate with the private key.
// This certificate has Oid for both TCP server and TCP client.
using var cert = CertificateOperations.IssueSignedCert(rootCert,
    CertificateOperations.KeySizeInBits, "A test TLS cert",
    X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation |
    X509KeyUsageFlags.KeyEncipherment,
    new OidCollection
    {
        new Oid("1.3.6.1.5.5.7.3.1")/*id-kp-serverAuth*/,
        new Oid("1.3.6.1.5.5.7.3.2")/*id-kp-clientAuth*/
    },
    notBefore, notAfter, true);
// Get the certificate with the desired key storage flags.
using var newCert = CertificateOperations.GetCertWithStorageFlags(cert,
    X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet |
    X509KeyStorageFlags.Exportable);

See the complete sample codes at my GitHub repository, specifically at CertificateOperations.cs.