dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.91k stars 4.63k forks source link

Certificate Creation API #20887

Closed bartonjs closed 4 years ago

bartonjs commented 7 years ago

API Goals:

Non Goals:

API Proposal:

 namespace System.Security.Cryptography.X509Certificates
 {
+    public sealed partial class CertificateRequest
+    {
+        public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { }
+        public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { }
+        public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { }
+        public CertificateRequest(string subjectDistinguishedName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { }
+        public CertificateRequest(string subjectDistinguishedName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { }
+        public System.Collections.Generic.ICollection<System.Security.Cryptography.X509Certificates.X509Extension> CertificateExtensions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+        public System.Security.Cryptography.HashAlgorithmName HashAlgorithm { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+        public System.Security.Cryptography.X509Certificates.PublicKey PublicKey { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+        public System.Security.Cryptography.X509Certificates.X500DistinguishedName Subject { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+        public byte[] EncodePkcs10SigningRequest() { throw null; }
+        public byte[] EncodePkcs10SigningRequest(System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator) { throw null; }
+        public System.Security.Cryptography.X509Certificates.X509Certificate2 SelfSign(System.DateTimeOffset notBefore, System.DateTimeOffset notAfter) { throw null; }
+        public System.Security.Cryptography.X509Certificates.X509Certificate2 SelfSign(System.TimeSpan validityPeriod) { throw null; }
+        public System.Security.Cryptography.X509Certificates.X509Certificate2 Sign(System.Security.Cryptography.X509Certificates.X500DistinguishedName issuerName, System.Security.Cryptography.X509Certificates.X509SignatureGenerator generator, System.DateTimeOffset notBefore, System.DateTimeOffset notAfter, byte[] serialNumber) { throw null; }
+        public System.Security.Cryptography.X509Certificates.X509Certificate2 Sign(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.DateTimeOffset notBefore, System.DateTimeOffset notAfter, byte[] serialNumber) { throw null; }
+        public System.Security.Cryptography.X509Certificates.X509Certificate2 Sign(System.Security.Cryptography.X509Certificates.X509Certificate2 issuerCertificate, System.TimeSpan validityPeriod, byte[] serialNumber) { throw null; }
+    }
     public static partial class DSACertificateExtensions
     {
+        public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateCopyWithPrivateKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.Security.Cryptography.DSA privateKey) { throw null; }
         public static System.Security.Cryptography.DSA GetDSAPrivateKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
         public static System.Security.Cryptography.DSA GetDSAPublicKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
     }
     public static partial class ECDsaCertificateExtensions
     {
+        public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateCopyWithPrivateKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.Security.Cryptography.ECDsa privateKey) { throw null; }
         public static System.Security.Cryptography.ECDsa GetECDsaPrivateKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
         public static System.Security.Cryptography.ECDsa GetECDsaPublicKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
     }
     public static partial class RSACertificateExtensions
     {
+        public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateCopyWithPrivateKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.Security.Cryptography.RSA privateKey) { throw null; }
         public static System.Security.Cryptography.RSA GetRSAPrivateKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
         public static System.Security.Cryptography.RSA GetRSAPublicKey(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
     }
+    public sealed partial class SubjectAltNameBuilder
+    {
+        public SubjectAltNameBuilder() { }
+        public void AddDnsName(string dnsName) { }
+        public void AddEmailAddress(string emailAddress) { }
+        public void AddIpAddress(System.Net.IPAddress ipAddress) { }
+        public void AddUri(System.Uri uri) { }
+        public void AddUserPrincipalName(string upn) { }
+        public System.Security.Cryptography.X509Certificates.X509Extension BuildExtension() { throw null; }
+        public System.Security.Cryptography.X509Certificates.X509Extension BuildExtension(bool critical) { throw null; }
+    }
+    public abstract partial class X509SignatureGenerator
+    {
+        protected X509SignatureGenerator() { }
+        public System.Security.Cryptography.X509Certificates.PublicKey PublicKey { get { throw null; } }
+        protected abstract System.Security.Cryptography.X509Certificates.PublicKey BuildPublicKey();
+        public static System.Security.Cryptography.X509Certificates.X509SignatureGenerator CreateForECDsa(System.Security.Cryptography.ECDsa key) { throw null; }
+        public static System.Security.Cryptography.X509Certificates.X509SignatureGenerator CreateForRSA(System.Security.Cryptography.RSA key, System.Security.Cryptography.RSASignaturePadding signaturePadding) { throw null; }
+        public abstract byte[] GetSignatureAlgorithmIdentifier(System.Security.Cryptography.HashAlgorithmName hashAlgorithm);
+        public abstract byte[] SignData(byte[] data, System.Security.Cryptography.HashAlgorithmName hashAlgorithm);
+    }

Changes from original version:

bartonjs commented 7 years ago

Example usage:

using System;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509CertificateCreation;
using System.Security.Cryptography.X509Certificates;

namespace CertReq
{
    public static class Samples
    {
        public static X509Certificate2 CreateRoot(string name)
        {
            // Creates a certificate roughly equivalent to 
            // makecert -r -n "{name}" -a sha256 -cy authority
            // 
            using (RSA rsa = RSA.Create())
            {
                var generator = new RSAPkcs1X509SignatureGenerator(rsa, HashAlgorithmName.SHA256);
                var request = new CertificateRequest { Subject = new X500DistinguishedName(name) };

                request.CertificateExtensions.Add(
                    new X509BasicConstraintsExtension(true, false, 0, true));

                // makecert will add an authority key identifier extension, which .NET doesn't
                // have out of the box.
                //
                // It does not add a subject key identifier extension, so we won't, either.
                return request.SelfSign(
                    generator,
                    DateTimeOffset.UtcNow,
                    // makecert's fixed default end-date.
                    new DateTimeOffset(2039, 12, 31, 23, 59, 59, TimeSpan.Zero),
                    X509KeyStorageFlags.DefaultKeySet);
            }
        }

        public static X509Certificate2 CreateTlsClient(string name, X509Certificate2 issuer, SubjectAltNameBuilder altNames)
        {
            using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384))
            {
                var selfGenerator = new ECDsaX509SignatureGenerator(ecdsa, HashAlgorithmName.SHA384);

                var request = new CertificateRequest
                {
                    Subject = new X500DistinguishedName(name),
                    PublicKey = selfGenerator.PublicKey,
                };

                request.CertificateExtensions.Add(
                    new X509BasicConstraintsExtension(false, false, 0, false));
                request.CertificateExtensions.Add(
                    new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
                request.CertificateExtensions.Add(
                    new X509EnhancedKeyUsageExtension(
                        new OidCollection { new Oid("1.3.6.1.5.5.7.3.2") }, false));

                if (altNames != null)
                {
                    request.CertificateExtensions.Add(altNames.BuildExtension());
                }

                var generator = new IssuerSignatureGenerator(issuer, HashAlgorithmName.SHA384);
                byte[] serialNumber = new byte[8];

                using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
                {
                    rng.GetBytes(serialNumber);
                }

                byte[] certBytes = request.Sign(generator, TimeSpan.FromDays(90), serialNumber);

                return CertificateRequest.AssociatePrivateKey(certBytes, selfGenerator, X509KeyStorageFlags.DefaultKeySet);
            }
        }

        public static X509Certificate2 BuildLocalhostTlsSelfSignedServer()
        {
            SubjectAltNameBuilder sanBuilder = new SubjectAltNameBuilder();
            sanBuilder.AddIpAddress(IPAddress.Loopback);
            sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
            sanBuilder.AddDnsName("localhost");
            sanBuilder.AddDnsName("localhost.localdomain");
            sanBuilder.AddDnsName(Environment.MachineName);

            using (RSA rsa = RSA.Create())
            {
                var generator = new RSAPkcs1X509SignatureGenerator(rsa, HashAlgorithmName.SHA256);
                var request = new CertificateRequest { Subject = new X500DistinguishedName("CN=localhost") };

                request.CertificateExtensions.Add(
                    new X509EnhancedKeyUsageExtension(
                        new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));

                request.CertificateExtensions.Add(sanBuilder.BuildExtension());

                return request.SelfSign(generator, TimeSpan.FromDays(90), X509KeyStorageFlags.DefaultKeySet);
            }
        }

        public static byte[] CreateCertificateRenewal(AsymmetricAlgorithm newKey, X509Certificate2 currentCert)
        {
            // Getting, and persisting, `newKey` is out of scope here.
            X509SignatureGenerator generator = null;

            if (newKey is ECDsa)
            {
                generator = new ECDsaX509SignatureGenerator((ECDsa)newKey, HashAlgorithmName.SHA384);
            }
            else if (newKey is RSA)
            {
                generator = new RSAPkcs1X509SignatureGenerator((RSA)newKey, HashAlgorithmName.SHA256);
            }

            CertificateRequest request = new CertificateRequest { Subject = currentCert.SubjectName };

            foreach (X509Extension extension in currentCert.Extensions)
            {
                request.CertificateExtensions.Add(extension);
            }

            // Send this to the CA you're requesting to sign your certificate.
            return request.EncodeSigningRequest(generator);
       }

        public static X509Certificate2 RenewCertificate(X509Certificate2 currentCert)
        {
            using (RSA rsa = RSA.Create())
            {
                byte[] certificateSigningRequest = CreateCertificateRenewal(rsa, currentCert);

                byte[] signedCertificate = SendRequestToCAAndGetResponse(certificateSigningRequest);

                return CertificateRequest.AssociatePrivateKey(
                    signedCertificate,
                    new RSAPkcs1X509SignatureGenerator(rsa, HashAlgorithmName.SHA256),
                    X509KeyStorageFlags.DefaultKeySet);
            }
        }
    }
}
bartonjs commented 7 years ago

@terrajobst Did you want to record the initial review notes?

terrajobst commented 7 years ago

Comments:

Other than that, it looks OK to me, considering our crypto stack will never win a price for usability :-)

bartonjs commented 7 years ago

The samples get a lot smaller with the API revisions:

using System;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace CertReq
{
    public static class Samples
    {
        public static X509Certificate2 CreateRoot(string name)
        {
            // Creates a certificate roughly equivalent to 
            // makecert -r -n "{name}" -a sha256 -cy authority
            // 
            using (RSA rsa = RSA.Create())
            {
                var request = new CertificateRequest(name, rsa, HashAlgorithmName.SHA256);

                request.CertificateExtensions.Add(
                    new X509BasicConstraintsExtension(true, false, 0, true));

                // makecert will add an authority key identifier extension, which .NET doesn't
                // have out of the box.
                //
                // It does not add a subject key identifier extension, so we won't, either.
                return request.SelfSign(
                    DateTimeOffset.UtcNow,
                    // makecert's fixed default end-date.
                    new DateTimeOffset(2039, 12, 31, 23, 59, 59, TimeSpan.Zero));
            }
        }

        public static X509Certificate2 CreateTlsClient(string name, X509Certificate2 issuer, SubjectAltNameBuilder altNames)
        {
            using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384))
            {
                var request = new CertificateRequest(name, ecdsa, HashAlgorithmName.SHA384);

                request.CertificateExtensions.Add(
                    new X509BasicConstraintsExtension(false, false, 0, false));
                request.CertificateExtensions.Add(
                    new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
                request.CertificateExtensions.Add(
                    new X509EnhancedKeyUsageExtension(
                        new OidCollection { new Oid("1.3.6.1.5.5.7.3.2") }, false));

                if (altNames != null)
                {
                    request.CertificateExtensions.Add(altNames.BuildExtension());
                }

                byte[] serialNumber = new byte[8];

                using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
                {
                    rng.GetBytes(serialNumber);
                }

                X509Certificate2 signedCert = request.Sign(
                    issuer,
                    TimeSpan.FromDays(90),
                    serialNumber);

                return signedCert.CreateCopyWithPrivateKey(ecdsa);
            }
        }

        public static X509Certificate2 BuildLocalhostTlsSelfSignedServer()
        {
            SubjectAltNameBuilder sanBuilder = new SubjectAltNameBuilder();
            sanBuilder.AddIpAddress(IPAddress.Loopback);
            sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
            sanBuilder.AddDnsName("localhost");
            sanBuilder.AddDnsName("localhost.localdomain");
            sanBuilder.AddDnsName(Environment.MachineName);

            using (RSA rsa = RSA.Create())
            {
                var request = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256);

                request.CertificateExtensions.Add(
                    new X509EnhancedKeyUsageExtension(
                        new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));

                request.CertificateExtensions.Add(sanBuilder.BuildExtension());

                return request.SelfSign(TimeSpan.FromDays(90));
            }
        }

        public static byte[] CreateCertificateRenewal(RSA newKey, X509Certificate2 currentCert)
        {
            // Getting, and persisting, `newKey` is out of scope here.

            var request = new CertificateRequest(
                currentCert.SubjectName,
                newKey,
                HashAlgorithmName.SHA256);

            foreach (X509Extension extension in currentCert.Extensions)
            {
                request.CertificateExtensions.Add(extension);
            }

            // Send this to the CA you're requesting to sign your certificate.
            return request.EncodePkcs10SigningRequest();
        }

        public static X509Certificate2 RenewCertificate(X509Certificate2 currentCert)
        {
            using (RSA rsa = RSA.Create())
            {
                byte[] certificateSigningRequest = CreateCertificateRenewal(rsa, currentCert);

                X509Certificate2 signedCertificate = SendRequestToCAAndGetResponse(certificateSigningRequest);

                return signedCertificate.CreateCopyWithPrivateKey(rsa);
            }
        }
    }
}
bartonjs commented 7 years ago

@terrajobst Notes of what changed after the review meeting (and a couple of hours more whiteboarding with @morganbr and @ChadNedzlek) is with the updated API proposal in the top entry.

morganbr commented 7 years ago

@bartonjs , this looks very good. I really like that the samples now really only contain business logic rather than ceremony to use the class for the major scenarios our users are likely to have.

bartonjs commented 7 years ago

Based on new data from Windows (and their lack of support for FIPS 186-3 DSA certificates) I'm going to pull the DSA typed constructor and leave DSA as a "power user" scenario (custom X509SignatureGenerator class, etc)

terrajobst commented 7 years ago

Review feedback:

public sealed partial class CertificateRequest
{
    public CertificateRequest(X500DistinguishedName subjectName, ECDsa key, HashAlgorithmName hashAlgorithm);
    public CertificateRequest(X500DistinguishedName subjectName, RSA key, HashAlgorithmName hashAlgorithm);
    public CertificateRequest(X500DistinguishedName subjectName, PublicKey publicKey, HashAlgorithmName hashAlgorithm);
    public CertificateRequest(string subjectName, ECDsa key, HashAlgorithmName hashAlgorithm);
    public CertificateRequest(string subjectName, RSA key, HashAlgorithmName hashAlgorithm);
    public Collection<X509Extension> CertificateExtensions { get; }
    public HashAlgorithmName HashAlgorithm { get; }
    public PublicKey PublicKey { get; }
    public X500DistinguishedName SubjectName { get; }
    public byte[] CreateSigningRequest();
    public byte[] CreateSigningRequest(X509SignatureGenerator signatureGenerator);
    public X509Certificate2 CreateSelfSigned(DateTimeOffset notBefore, DateTimeOffset notAfter);
    public X509Certificate2 Create(X500DistinguishedName issuerName, X509SignatureGenerator generator, DateTimeOffset notBefore, DateTimeOffset notAfter, byte[] serialNumber);
    public X509Certificate2 Create(X509Certificate2 issuerCertificate, DateTimeOffset notBefore, DateTimeOffset notAfter, byte[] serialNumber);
 }