New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Certificate Creation API #17892

Closed
bartonjs opened this Issue Apr 4, 2017 · 8 comments

Comments

Projects
None yet
3 participants
@bartonjs
Copy link
Member

bartonjs commented Apr 4, 2017

API Goals:

  • Be able to create self-signed certificates
  • Be able to programmatically chain sign certificates
  • Be able to issue a PKCS10 Certificate/Certification Signing Request to a CA.

Non Goals:

  • To be a full CA product

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:

  • CertificateRequest has required ctor parameters now.
    • SubjectName: Either an X500DistinguishedName or a string (to be parsed as an X500DN)
    • Key
      • A type-limited AsymmetricAlgorithm:
        • Used as a private key for SelfSign and EncodePkcs10SigningRequest()
        • Used as a public key for EncodePkcs10SigningRequest(X509SignatureGenerator) or (other)Sign
      • A PublicKey prebuilt object. Not compatible with the places an AsymmetricAlgorithm would be used as a private key, but allows for algorithms whose public keys we don't support to be used.
    • HashAlgorithmName: The hash/digest algorithm which will be used in Sign, SelfSign, or EncodePkcs10SigningRequest.
  • HashAlgorithmName moves out of X509SignatureGenerator constructor
    • The members of X509SignatureGenerator which needed the value now take it as parameters.
  • X509SignatureGenerator derived types are now internal, and accessible via static methods on the base class, reducing the number of public types required.
  • SelfSign no longer needs a X509KeyStorageFlags. (Since we're in the same assembly as the certificates we can play with pointers; and since we're inbox we can use P/Invokes on non-Windows).
  • CertificateRequest.AssociatePrivateKey is now three extension methods on X509Certificate2 (CreateCopyWithPrivateKey).
  • Added "Pkcs10" into the middle of "Encode[Pkcs10]SigningRequest", to make the serialization format more obvious.

@bartonjs bartonjs added this to the 2.0.0 milestone Apr 4, 2017

@bartonjs bartonjs self-assigned this Apr 4, 2017

@bartonjs

This comment has been minimized.

Copy link
Member Author

bartonjs commented Apr 4, 2017

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

This comment has been minimized.

Copy link
Member Author

bartonjs commented Apr 4, 2017

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

@terrajobst

This comment has been minimized.

Copy link
Member

terrajobst commented Apr 4, 2017

Comments:

  • We should use the existing namespace to System.Security.Cryptography.X509Certificates
  • We should probably add this API to System.Security.Cryptography.X509Certificates.dll, which makes it .NET Core only for now. If we want to expose this API in .NET Standard, we'll have to add it in the right spots. We don't want to create OOB libraries as stop gaps because it just proliferates the number of NuGet packages and creates a lot of complexity in order to provide type unification when we have add the types in-box.
  • We should consider making Samples.CreateRoot a convenience API the shipped API. Reason being that most people seem to have the problem of being able to cook up cert out of thin air for testing purposes.

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

@bartonjs

This comment has been minimized.

Copy link
Member Author

bartonjs commented Apr 6, 2017

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

This comment has been minimized.

Copy link
Member Author

bartonjs commented Apr 6, 2017

@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

This comment has been minimized.

Copy link
Contributor

morganbr commented Apr 6, 2017

@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

This comment has been minimized.

Copy link
Member Author

bartonjs commented Apr 7, 2017

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

This comment has been minimized.

Copy link
Member

terrajobst commented Apr 11, 2017

Review feedback:

  • It would be nice if constructor arguments correspond to public properties
  • It would be nice if the methods constructing the result of the cert request would share a common prefix
  • Change CreateCopyWithPrivateKey to CopyWithPrivateKey or CreateWithPrivateKey. CreateCopy is a tautology.
  • Change SubjectAltNameBuilder to SubjectAlternativeNameBuilder. We don't do abbreviations.
  • Change BuildExtension to just Build
  • Replace BuildExtension overloads with optional parameter
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);
 }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment