Repository: smallstep/crypto
Pull request #41 (2022-03-31 00:44:00)

Add ExtraNames
### Description
This PR adds ExtraNames to X.509 subject and issuers, so they can be templatized. One use of this is adding the deprecated DN emailAddress.

This PR also exposes the root signer in minica and fixes linter errors with Go 1.18.


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 02a390e..e8f6eef 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-20.04
     strategy:
       matrix:
-        go: [ '1.16', '1.17' ]
+        go: [ '1.17', '1.18' ]
     steps:
       -
         name: Checkout
@@ -26,7 +26,7 @@ jobs:
         name: golangci-lint
         uses: golangci/golangci-lint-action@v2
         with:
-          version: 'v1.44.0'
+          version: 'v1.45.2'
           args: --timeout=30m
       -
         name: Test, Build
diff --git a/minica/minica.go b/minica/minica.go
index 0385a58..d850257 100644
--- a/minica/minica.go
+++ b/minica/minica.go
@@ -14,6 +14,7 @@ import (
 // CA is the implementation of a simple X.509 and SSH CA.
 type CA struct {
 	Root          *x509.Certificate
+	RootSigner    crypto.Signer
 	Intermediate  *x509.Certificate
 	Signer        crypto.Signer
 	SSHHostSigner ssh.Signer
@@ -92,6 +93,7 @@ func New(opts ...Option) (*CA, error) {
 
 	return &CA{
 		Root:          root,
+		RootSigner:    rootSigner,
 		Intermediate:  intermediate,
 		Signer:        intSigner,
 		SSHHostSigner: sshHostSigner,
diff --git a/x509util/name.go b/x509util/name.go
index 1462e08..190fa69 100644
--- a/x509util/name.go
+++ b/x509util/name.go
@@ -3,6 +3,7 @@ package x509util
 import (
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"encoding/asn1"
 	"encoding/json"
 
 	"github.com/pkg/errors"
@@ -11,15 +12,31 @@ import (
 // Name is the JSON representation of X.501 type Name, used in the X.509 subject
 // and issuer fields.
 type Name struct {
-	Country            MultiString `json:"country,omitempty"`
-	Organization       MultiString `json:"organization,omitempty"`
-	OrganizationalUnit MultiString `json:"organizationalUnit,omitempty"`
-	Locality           MultiString `json:"locality,omitempty"`
-	Province           MultiString `json:"province,omitempty"`
-	StreetAddress      MultiString `json:"streetAddress,omitempty"`
-	PostalCode         MultiString `json:"postalCode,omitempty"`
-	SerialNumber       string      `json:"serialNumber,omitempty"`
-	CommonName         string      `json:"commonName,omitempty"`
+	Country            MultiString         `json:"country,omitempty"`
+	Organization       MultiString         `json:"organization,omitempty"`
+	OrganizationalUnit MultiString         `json:"organizationalUnit,omitempty"`
+	Locality           MultiString         `json:"locality,omitempty"`
+	Province           MultiString         `json:"province,omitempty"`
+	StreetAddress      MultiString         `json:"streetAddress,omitempty"`
+	PostalCode         MultiString         `json:"postalCode,omitempty"`
+	SerialNumber       string              `json:"serialNumber,omitempty"`
+	CommonName         string              `json:"commonName,omitempty"`
+	ExtraNames         []DistinguishedName `json:"extraNames,omitempty"`
+}
+
+func newName(n pkix.Name) Name {
+	return Name{
+		Country:            n.Country,
+		Organization:       n.Organization,
+		OrganizationalUnit: n.OrganizationalUnit,
+		Locality:           n.Locality,
+		Province:           n.Province,
+		StreetAddress:      n.StreetAddress,
+		PostalCode:         n.PostalCode,
+		SerialNumber:       n.SerialNumber,
+		CommonName:         n.CommonName,
+		ExtraNames:         newDistinguisedNames(n.ExtraNames),
+	}
 }
 
 // UnmarshalJSON implements the json.Unmarshal interface and unmarshals a JSON
@@ -43,17 +60,7 @@ func (n *Name) UnmarshalJSON(data []byte) error {
 type Subject Name
 
 func newSubject(n pkix.Name) Subject {
-	return Subject{
-		Country:            n.Country,
-		Organization:       n.Organization,
-		OrganizationalUnit: n.OrganizationalUnit,
-		Locality:           n.Locality,
-		Province:           n.Province,
-		StreetAddress:      n.StreetAddress,
-		PostalCode:         n.PostalCode,
-		SerialNumber:       n.SerialNumber,
-		CommonName:         n.CommonName,
-	}
+	return Subject(newName(n))
 }
 
 // UnmarshalJSON implements the json.Unmarshal interface and unmarshals a JSON
@@ -79,6 +86,7 @@ func (s Subject) Set(c *x509.Certificate) {
 		PostalCode:         s.PostalCode,
 		SerialNumber:       s.SerialNumber,
 		CommonName:         s.CommonName,
+		ExtraNames:         fromDistinguisedNames(s.ExtraNames),
 	}
 }
 
@@ -86,17 +94,7 @@ func (s Subject) Set(c *x509.Certificate) {
 type Issuer Name
 
 func newIssuer(n pkix.Name) Issuer {
-	return Issuer{
-		Country:            n.Country,
-		Organization:       n.Organization,
-		OrganizationalUnit: n.OrganizationalUnit,
-		Locality:           n.Locality,
-		Province:           n.Province,
-		StreetAddress:      n.StreetAddress,
-		PostalCode:         n.PostalCode,
-		SerialNumber:       n.SerialNumber,
-		CommonName:         n.CommonName,
-	}
+	return Issuer(newName(n))
 }
 
 // UnmarshalJSON implements the json.Unmarshal interface and unmarshals a JSON
@@ -122,5 +120,35 @@ func (i Issuer) Set(c *x509.Certificate) {
 		PostalCode:         i.PostalCode,
 		SerialNumber:       i.SerialNumber,
 		CommonName:         i.CommonName,
+		ExtraNames:         fromDistinguisedNames(i.ExtraNames),
+	}
+}
+
+// DistinguishedName mirrors the ASN.1 structure AttributeTypeAndValue in RFC
+// 5280, Section 4.1.2.4.
+type DistinguishedName struct {
+	Type  ObjectIdentifier `json:"type"`
+	Value interface{}      `json:"value"`
+}
+
+func newDistinguisedNames(atvs []pkix.AttributeTypeAndValue) []DistinguishedName {
+	var extraNames []DistinguishedName
+	for _, atv := range atvs {
+		extraNames = append(extraNames, DistinguishedName{
+			Type:  ObjectIdentifier(atv.Type),
+			Value: atv.Value,
+		})
+	}
+	return extraNames
+}
+
+func fromDistinguisedNames(dns []DistinguishedName) []pkix.AttributeTypeAndValue {
+	var atvs []pkix.AttributeTypeAndValue
+	for _, dn := range dns {
+		atvs = append(atvs, pkix.AttributeTypeAndValue{
+			Type:  asn1.ObjectIdentifier(dn.Type),
+			Value: dn.Value,
+		})
 	}
+	return atvs
 }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b252b0f..a84a707 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-20.04
     strategy:
       matrix:
-        go: [ '1.16', '1.17' ]
+        go: [ '1.17', '1.18' ]
     steps:
       -
         name: Checkout
@@ -28,7 +28,7 @@ jobs:
         name: golangci-lint
         uses: golangci/golangci-lint-action@v2
         with:
-          version: 'v1.44.0'
+          version: 'v1.45.2'
           args: --timeout=30m
       -
         name: Test, Build
@@ -37,7 +37,7 @@ jobs:
       -
         name: Codecov
         uses: codecov/codecov-action@v1.2.1
-        if: matrix.go == '1.17'
+        if: matrix.go == '1.18'
         with:
           file: ./coverage.out
           name: codecov-umbrella
diff --git a/x509util/certpool_test.go b/x509util/certpool_test.go
index 3a00032..bd6d685 100644
--- a/x509util/certpool_test.go
+++ b/x509util/certpool_test.go
@@ -30,6 +30,9 @@ func TestReadCertPool(t *testing.T) {
 				return
 			}
 			if got != nil {
+				// nolint:staticcheck // there's no other way to compare two
+				// certpools, https://github.com/golang/go/issues/46057 might
+				// fix this.
 				subjects := got.Subjects()
 				if !reflect.DeepEqual(subjects, tt.wantSubjects) {
 					t.Errorf("x509.CertPool.Subjects() got = %v, want %v", subjects, tt.wantSubjects)
diff --git a/x509util/name_test.go b/x509util/name_test.go
index e2c88a4..2fc1b9a 100644
--- a/x509util/name_test.go
+++ b/x509util/name_test.go
@@ -3,10 +3,57 @@ package x509util
 import (
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"encoding/asn1"
 	"reflect"
 	"testing"
 )
 
+func Test_newName(t *testing.T) {
+	type args struct {
+		n pkix.Name
+	}
+	tests := []struct {
+		name string
+		args args
+		want Name
+	}{
+		{"ok", args{pkix.Name{
+			Country:            []string{"The country"},
+			Organization:       []string{"The organization"},
+			OrganizationalUnit: []string{"The organizationalUnit 1", "The organizationalUnit 2"},
+			Locality:           []string{"The locality 1", "The locality 2"},
+			Province:           []string{"The province"},
+			StreetAddress:      []string{"The streetAddress"},
+			PostalCode:         []string{"The postalCode"},
+			SerialNumber:       "The serialNumber",
+			CommonName:         "The commonName",
+			ExtraNames: []pkix.AttributeTypeAndValue{
+				{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			},
+		}}, Name{
+			Country:            []string{"The country"},
+			Organization:       []string{"The organization"},
+			OrganizationalUnit: []string{"The organizationalUnit 1", "The organizationalUnit 2"},
+			Locality:           []string{"The locality 1", "The locality 2"},
+			Province:           []string{"The province"},
+			StreetAddress:      []string{"The streetAddress"},
+			PostalCode:         []string{"The postalCode"},
+			SerialNumber:       "The serialNumber",
+			CommonName:         "The commonName",
+			ExtraNames: []DistinguishedName{
+				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := newName(tt.args.n); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("newName() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
 func TestName_UnmarshalJSON(t *testing.T) {
 	type args struct {
 		data []byte
@@ -29,7 +76,8 @@ func TestName_UnmarshalJSON(t *testing.T) {
 			"streetAddress": "The streetAddress",
 			"postalCode": "The postalCode",
 			"serialNumber": "The serialNumber",
-			"commonName": "The commonName"
+			"commonName": "The commonName",
+			"extraNames": [{"type":"1.2.840.113549.1.9.1", "value":"jane@example.com"}]
 		}`)}, Name{
 			Country:            []string{"The country"},
 			Organization:       []string{"The organization"},
@@ -40,6 +88,9 @@ func TestName_UnmarshalJSON(t *testing.T) {
 			PostalCode:         []string{"The postalCode"},
 			SerialNumber:       "The serialNumber",
 			CommonName:         "The commonName",
+			ExtraNames: []DistinguishedName{
+				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			},
 		}, false},
 		{"number", args{[]byte("1234")}, Name{}, true},
 		{"badJSON", args{[]byte("'badJSON'")}, Name{}, true},
@@ -76,6 +127,9 @@ func Test_newSubject(t *testing.T) {
 			PostalCode:         []string{"The postalCode"},
 			SerialNumber:       "The serialNumber",
 			CommonName:         "The commonName",
+			ExtraNames: []pkix.AttributeTypeAndValue{
+				{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			},
 		}}, Subject{
 			Country:            []string{"The country"},
 			Organization:       []string{"The organization"},
@@ -86,6 +140,9 @@ func Test_newSubject(t *testing.T) {
 			PostalCode:         []string{"The postalCode"},
 			SerialNumber:       "The serialNumber",
 			CommonName:         "The commonName",
+			ExtraNames: []DistinguishedName{
+				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			},
 		}},
 	}
 	for _, tt := range tests {
@@ -282,7 +339,8 @@ func TestIssuer_UnmarshalJSON(t *testing.T) {
 			"streetAddress": "The streetAddress",
 			"postalCode": "The postalCode",
 			"serialNumber": "The serialNumber",
-			"commonName": "The commonName"
+			"commonName": "The commonName",
+			"extraNames": [{"type":"1.2.840.113549.1.9.1", "value":"jane@example.com"}]
 		}`)}, Issuer{
 			Country:            []string{"The country"},
 			Organization:       []string{"The organization"},
@@ -293,6 +351,9 @@ func TestIssuer_UnmarshalJSON(t *testing.T) {
 			PostalCode:         []string{"The postalCode"},
 			SerialNumber:       "The serialNumber",
 			CommonName:         "The commonName",
+			ExtraNames: []DistinguishedName{
+				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			},
 		}, false},
 		{"number", args{[]byte("1234")}, Issuer{}, true},
 		{"badJSON", args{[]byte("'badJSON'")}, Issuer{}, true},
@@ -321,6 +382,7 @@ func TestIssuer_Set(t *testing.T) {
 		PostalCode         MultiString
 		SerialNumber       string
 		CommonName         string
+		ExtraNames         []DistinguishedName
 	}
 	type args struct {
 		c *x509.Certificate
@@ -341,6 +403,9 @@ func TestIssuer_Set(t *testing.T) {
 			PostalCode:         []string{"The postalCode"},
 			SerialNumber:       "The serialNumber",
 			CommonName:         "The commonName",
+			ExtraNames: []DistinguishedName{
+				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			},
 		}, args{&x509.Certificate{}}, &x509.Certificate{
 			Issuer: pkix.Name{
 				Country:            []string{"The country"},
@@ -352,6 +417,9 @@ func TestIssuer_Set(t *testing.T) {
 				PostalCode:         []string{"The postalCode"},
 				SerialNumber:       "The serialNumber",
 				CommonName:         "The commonName",
+				ExtraNames: []pkix.AttributeTypeAndValue{
+					{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+				},
 			},
 		}},
 		{"overwrite", fields{
@@ -374,6 +442,7 @@ func TestIssuer_Set(t *testing.T) {
 				PostalCode:         tt.fields.PostalCode,
 				SerialNumber:       tt.fields.SerialNumber,
 				CommonName:         tt.fields.CommonName,
+				ExtraNames:         tt.fields.ExtraNames,
 			}
 			i.Set(tt.args.c)
 			if !reflect.DeepEqual(tt.args.c, tt.want) {
@@ -382,3 +451,53 @@ func TestIssuer_Set(t *testing.T) {
 		})
 	}
 }
+
+func Test_newDistinguisedNames(t *testing.T) {
+	type args struct {
+		atvs []pkix.AttributeTypeAndValue
+	}
+	tests := []struct {
+		name string
+		args args
+		want []DistinguishedName
+	}{
+		{"ok", args{[]pkix.AttributeTypeAndValue{
+			{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+		}}, []DistinguishedName{
+			{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+		}},
+		{"ok nil", args{nil}, nil},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := newDistinguisedNames(tt.args.atvs); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("newDistinguisedNames() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_fromDistinguisedNames(t *testing.T) {
+	type args struct {
+		dns []DistinguishedName
+	}
+	tests := []struct {
+		name string
+		args args
+		want []pkix.AttributeTypeAndValue
+	}{
+		{"ok", args{[]DistinguishedName{
+			{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+		}}, []pkix.AttributeTypeAndValue{
+			{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+		}},
+		{"ok nil", args{nil}, nil},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := fromDistinguisedNames(tt.args.dns); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("fromDistinguisedNames() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}