Repository: smallstep/crypto
Pull request #42 (2022-05-16 20:17:15)

Populate ExtraNames from Names
### Description

This commit will populate the subject ExtraNames in a certificate request from the information present in Names as long as it is not one of the default fields. This way, we can extract non-default fields as well as populate them in the final certificate if necessary.

It also uses the proper type for the deprecated email address attribute in the subject.

Related to smallstep/certificates#916


Patch:
diff --git a/x509util/extensions.go b/x509util/extensions.go
index b0d6f5b..94ceb7b 100644
--- a/x509util/extensions.go
+++ b/x509util/extensions.go
@@ -84,7 +84,6 @@ func newExtensions(extensions []pkix.Extension) []Extension {
 		ret[i] = newExtension(e)
 	}
 	return ret
-
 }
 
 // Set adds the extension to the given X509 certificate.
diff --git a/x509util/name.go b/x509util/name.go
index 190fa69..e0cde29 100644
--- a/x509util/name.go
+++ b/x509util/name.go
@@ -9,6 +9,24 @@ import (
 	"github.com/pkg/errors"
 )
 
+// attributeTypeNames are the subject attributes managed by Go and this package.
+// newExtraNames will populate .Insecure.CR.ExtraNames with the attributes not
+// present on this map.
+var attributeTypeNames = map[string]string{
+	"2.5.4.6":  "C",
+	"2.5.4.10": "O",
+	"2.5.4.11": "OU",
+	"2.5.4.3":  "CN",
+	"2.5.4.5":  "SERIALNUMBER",
+	"2.5.4.7":  "L",
+	"2.5.4.8":  "ST",
+	"2.5.4.9":  "STREET",
+	"2.5.4.17": "POSTALCODE",
+}
+
+// oidEmailAddress is the oid of the deprecated emailAddress in the subject.
+var oidEmailAddress = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}
+
 // Name is the JSON representation of X.501 type Name, used in the X.509 subject
 // and issuer fields.
 type Name struct {
@@ -35,7 +53,7 @@ func newName(n pkix.Name) Name {
 		PostalCode:         n.PostalCode,
 		SerialNumber:       n.SerialNumber,
 		CommonName:         n.CommonName,
-		ExtraNames:         newDistinguisedNames(n.ExtraNames),
+		ExtraNames:         newExtraNames(n.Names),
 	}
 }
 
@@ -86,7 +104,7 @@ func (s Subject) Set(c *x509.Certificate) {
 		PostalCode:         s.PostalCode,
 		SerialNumber:       s.SerialNumber,
 		CommonName:         s.CommonName,
-		ExtraNames:         fromDistinguisedNames(s.ExtraNames),
+		ExtraNames:         fromDistinguishedNames(s.ExtraNames),
 	}
 }
 
@@ -120,7 +138,7 @@ func (i Issuer) Set(c *x509.Certificate) {
 		PostalCode:         i.PostalCode,
 		SerialNumber:       i.SerialNumber,
 		CommonName:         i.CommonName,
-		ExtraNames:         fromDistinguisedNames(i.ExtraNames),
+		ExtraNames:         fromDistinguishedNames(i.ExtraNames),
 	}
 }
 
@@ -131,24 +149,44 @@ type DistinguishedName struct {
 	Value interface{}      `json:"value"`
 }
 
-func newDistinguisedNames(atvs []pkix.AttributeTypeAndValue) []DistinguishedName {
+// newExtraNames returns a list of DistinguishedName with the attributes not
+// present in attributeTypeNames.
+func newExtraNames(atvs []pkix.AttributeTypeAndValue) []DistinguishedName {
 	var extraNames []DistinguishedName
 	for _, atv := range atvs {
-		extraNames = append(extraNames, DistinguishedName{
-			Type:  ObjectIdentifier(atv.Type),
-			Value: atv.Value,
-		})
+		if _, ok := attributeTypeNames[atv.Type.String()]; !ok {
+			extraNames = append(extraNames, DistinguishedName{
+				Type:  ObjectIdentifier(atv.Type),
+				Value: atv.Value,
+			})
+		}
 	}
 	return extraNames
 }
 
-func fromDistinguisedNames(dns []DistinguishedName) []pkix.AttributeTypeAndValue {
+// fromDistinguishedNames converts a list of DistinguishedName to
+// []pkix.AttributeTypeAndValue. Note that this method has a special case to
+// encode the deprecated emailAddress field (1.2.840.113549.1.9.1).
+func fromDistinguishedNames(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,
-		})
+		typ := asn1.ObjectIdentifier(dn.Type)
+		v, isString := dn.Value.(string)
+		if typ.Equal(oidEmailAddress) && isString {
+			atvs = append(atvs, pkix.AttributeTypeAndValue{
+				Type: typ,
+				Value: asn1.RawValue{
+					Class: asn1.ClassUniversal,
+					Tag:   asn1.TagIA5String,
+					Bytes: []byte(v),
+				},
+			})
+		} else {
+			atvs = append(atvs, pkix.AttributeTypeAndValue{
+				Type:  typ,
+				Value: dn.Value,
+			})
+		}
 	}
 	return atvs
 }


Test patch:
diff --git a/x509util/name_test.go b/x509util/name_test.go
index 2fc1b9a..6c017b6 100644
--- a/x509util/name_test.go
+++ b/x509util/name_test.go
@@ -27,8 +27,19 @@ func Test_newName(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"},
+			Names: []pkix.AttributeTypeAndValue{
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 6}, Value: "The country"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 10}, Value: "The organization"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 11}, Value: "The organizationalUnit 1"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 11}, Value: "The organizationalUnit 2"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "The commonName"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 5}, Value: "The serialNumber"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 7}, Value: "The locality 1"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 7}, Value: "The locality 2"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 8}, Value: "The province"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 9}, Value: "The streetAddress"},
+				{Type: asn1.ObjectIdentifier{2, 5, 4, 17}, Value: "The postalCode"},
+				{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
 			},
 		}}, Name{
 			Country:            []string{"The country"},
@@ -41,7 +52,7 @@ func Test_newName(t *testing.T) {
 			SerialNumber:       "The serialNumber",
 			CommonName:         "The commonName",
 			ExtraNames: []DistinguishedName{
-				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
 			},
 		}},
 	}
@@ -127,8 +138,8 @@ 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"},
+			Names: []pkix.AttributeTypeAndValue{
+				{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
 			},
 		}}, Subject{
 			Country:            []string{"The country"},
@@ -141,7 +152,7 @@ func Test_newSubject(t *testing.T) {
 			SerialNumber:       "The serialNumber",
 			CommonName:         "The commonName",
 			ExtraNames: []DistinguishedName{
-				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
 			},
 		}},
 	}
@@ -405,6 +416,7 @@ func TestIssuer_Set(t *testing.T) {
 			CommonName:         "The commonName",
 			ExtraNames: []DistinguishedName{
 				{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+				{Type: ObjectIdentifier{1, 2, 3, 4}, Value: "custom@example.com"},
 			},
 		}, args{&x509.Certificate{}}, &x509.Certificate{
 			Issuer: pkix.Name{
@@ -418,7 +430,8 @@ func TestIssuer_Set(t *testing.T) {
 				SerialNumber:       "The serialNumber",
 				CommonName:         "The commonName",
 				ExtraNames: []pkix.AttributeTypeAndValue{
-					{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+					{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
+					{Type: asn1.ObjectIdentifier{1, 2, 3, 4}, Value: "custom@example.com"},
 				},
 			},
 		}},
@@ -452,7 +465,7 @@ func TestIssuer_Set(t *testing.T) {
 	}
 }
 
-func Test_newDistinguisedNames(t *testing.T) {
+func Test_newExtraNames(t *testing.T) {
 	type args struct {
 		atvs []pkix.AttributeTypeAndValue
 	}
@@ -462,15 +475,18 @@ func Test_newDistinguisedNames(t *testing.T) {
 		want []DistinguishedName
 	}{
 		{"ok", args{[]pkix.AttributeTypeAndValue{
-			{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "The commonName"},
+			{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
+			{Type: asn1.ObjectIdentifier{1, 2, 3, 4}, Value: "custom@example.com"},
 		}}, []DistinguishedName{
-			{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
+			{Type: ObjectIdentifier{1, 2, 3, 4}, Value: "custom@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) {
+			if got := newExtraNames(tt.args.atvs); !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("newDistinguisedNames() = %v, want %v", got, tt.want)
 			}
 		})
@@ -488,14 +504,16 @@ func Test_fromDistinguisedNames(t *testing.T) {
 	}{
 		{"ok", args{[]DistinguishedName{
 			{Type: ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			{Type: ObjectIdentifier{1, 2, 3, 4}, Value: "custom@example.com"},
 		}}, []pkix.AttributeTypeAndValue{
-			{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: "jane@example.com"},
+			{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}, Value: asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagIA5String, Bytes: []byte("jane@example.com")}},
+			{Type: asn1.ObjectIdentifier{1, 2, 3, 4}, Value: "custom@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) {
+			if got := fromDistinguishedNames(tt.args.dns); !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("fromDistinguisedNames() = %v, want %v", got, tt.want)
 			}
 		})