Repository: smallstep/crypto
Pull request #11 (2020-10-26 19:29:47)

CreateCertificateTemplate
### Description

This PR adds a method to create an x509.Certificate from an x509.CertificateRequest. 


Patch:
diff --git a/x509util/certificate.go b/x509util/certificate.go
index 9c880e5..555d0b6 100644
--- a/x509util/certificate.go
+++ b/x509util/certificate.go
@@ -149,3 +149,24 @@ func CreateCertificate(template, parent *x509.Certificate, pub crypto.PublicKey,
 	}
 	return cert, nil
 }
+
+// CreateCertificateTemplate creates a X.509 certificate template from the given certificate request.
+func CreateCertificateTemplate(cr *x509.CertificateRequest) (*x509.Certificate, error) {
+	if err := cr.CheckSignature(); err != nil {
+		return nil, errors.Wrap(err, "error validating certificate request")
+	}
+	// Set SubjectAltName extension as critical if Subject is empty.
+	fixSubjectAltName(cr)
+
+	return &x509.Certificate{
+		Subject:            cr.Subject,
+		DNSNames:           cr.DNSNames,
+		EmailAddresses:     cr.EmailAddresses,
+		IPAddresses:        cr.IPAddresses,
+		URIs:               cr.URIs,
+		ExtraExtensions:    cr.Extensions,
+		PublicKey:          cr.PublicKey,
+		PublicKeyAlgorithm: cr.PublicKeyAlgorithm,
+		SignatureAlgorithm: 0,
+	}, nil
+}
diff --git a/x509util/certificate_request.go b/x509util/certificate_request.go
index dbba214..4c58eba 100644
--- a/x509util/certificate_request.go
+++ b/x509util/certificate_request.go
@@ -73,17 +73,7 @@ func NewCertificateRequest(signer crypto.Signer, opts ...Option) (*CertificateRe
 // Certificate.SignatureAlgorithm is set.
 func newCertificateRequest(cr *x509.CertificateRequest) *CertificateRequest {
 	// Set SubjectAltName extension as critical if Subject is empty.
-	if len(cr.Extensions) > 0 {
-		if asn1Subject, err := asn1.Marshal(cr.Subject.ToRDNSequence()); err == nil {
-			if bytes.Equal(asn1Subject, emptyASN1Subject) {
-				for i, ext := range cr.Extensions {
-					if ext.Id.Equal(oidExtensionSubjectAltName) {
-						cr.Extensions[i].Critical = true
-					}
-				}
-			}
-		}
-	}
+	fixSubjectAltName(cr)
 	return &CertificateRequest{
 		Version:            cr.Version,
 		Subject:            newSubject(cr.Subject),
@@ -181,3 +171,17 @@ func CreateCertificateRequest(commonName string, sans []string, signer crypto.Si
 	// This should not fail
 	return x509.ParseCertificateRequest(asn1Data)
 }
+
+// fixSubjectAltName makes sure to mark the SAN extension to critical if the
+// subject is empty.
+func fixSubjectAltName(cr *x509.CertificateRequest) {
+	if asn1Subject, err := asn1.Marshal(cr.Subject.ToRDNSequence()); err == nil {
+		if bytes.Equal(asn1Subject, emptyASN1Subject) {
+			for i, ext := range cr.Extensions {
+				if ext.Id.Equal(oidExtensionSubjectAltName) {
+					cr.Extensions[i].Critical = true
+				}
+			}
+		}
+	}
+}


Test patch:
diff --git a/x509util/certificate_test.go b/x509util/certificate_test.go
index e40c6d0..65c7dae 100644
--- a/x509util/certificate_test.go
+++ b/x509util/certificate_test.go
@@ -488,3 +488,68 @@ func TestCreateCertificate_criticalSANs(t *testing.T) {
 		})
 	}
 }
+
+func TestCreateCertificateTemplate(t *testing.T) {
+	cr1, _ := createCertificateRequest(t, "commonName", []string{"doe.com", "jane@doe.com", "1.2.3.4", "urn:uuid:2bbe86fc-a35e-4c68-a5cb-cb1060f57629"})
+	cr2, _ := createCertificateRequest(t, "", []string{"doe.com"})
+	cr3, _ := createCertificateRequest(t, "commonName", []string{})
+
+	fail, _ := createCertificateRequest(t, "commonName", []string{"doe.com"})
+	fail.Signature = []byte{1, 2, 3, 4}
+
+	type args struct {
+		cr *x509.CertificateRequest
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *x509.Certificate
+		wantErr bool
+	}{
+		{"ok", args{cr1}, &x509.Certificate{
+			Subject: pkix.Name{
+				CommonName: "commonName",
+				Names:      []pkix.AttributeTypeAndValue{{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "commonName"}},
+			},
+			PublicKey:          cr1.PublicKey,
+			PublicKeyAlgorithm: cr1.PublicKeyAlgorithm,
+			ExtraExtensions: []pkix.Extension{
+				{Id: oidExtensionSubjectAltName, Critical: false, Value: cr1.Extensions[0].Value},
+			},
+			DNSNames:       []string{"doe.com"},
+			EmailAddresses: []string{"jane@doe.com"},
+			IPAddresses:    []net.IP{net.ParseIP("1.2.3.4").To4()},
+			URIs:           []*url.URL{{Scheme: "urn", Opaque: "uuid:2bbe86fc-a35e-4c68-a5cb-cb1060f57629"}},
+		}, false},
+		{"ok critical", args{cr2}, &x509.Certificate{
+			Subject:            pkix.Name{},
+			PublicKey:          cr2.PublicKey,
+			PublicKeyAlgorithm: cr3.PublicKeyAlgorithm,
+			ExtraExtensions: []pkix.Extension{
+				{Id: oidExtensionSubjectAltName, Critical: true, Value: cr2.Extensions[0].Value},
+			},
+			DNSNames: []string{"doe.com"},
+		}, false},
+		{"ok no extensions", args{cr3}, &x509.Certificate{
+			Subject: pkix.Name{
+				CommonName: "commonName",
+				Names:      []pkix.AttributeTypeAndValue{{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "commonName"}},
+			},
+			PublicKey:          cr3.PublicKey,
+			PublicKeyAlgorithm: cr3.PublicKeyAlgorithm,
+		}, false},
+		{"fail", args{fail}, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := CreateCertificateTemplate(tt.args.cr)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("CreateCertificateTemplate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("CreateCertificateTemplate() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}