Skip to content

Commit

Permalink
Add support for extended SANs on certificate requests
Browse files Browse the repository at this point in the history
This commit adds supports for adding extended SANs like the permanent
identifier used in device attestation flows with CSRs. It was already
supported on certificates but not on certificate requests.

With this it will be possible to run:
  step certificate create --csr --template att.tpl \
  --kms "yubikey:?pin-value=123456" --key "yubikey:slot-id=82" \
  123456789 att.csr

Where att.tpl can be something like:
  {
    "subject": {{ toJson .Subject }},
    "sans": [{
      "type": "permanentIdentifier",
      "value": {{ toJson .Subject.CommonName }}
    }]
  }
  • Loading branch information
maraino committed Feb 3, 2023
1 parent 4173a61 commit 9215f10
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 26 deletions.
2 changes: 1 addition & 1 deletion x509util/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func NewCertificate(cr *x509.CertificateRequest, opts ...Option) (*Certificate,
// Generate the subjectAltName extension if the certificate contains SANs
// that are not supported in the Go standard library.
if cert.hasExtendedSANs() && !cert.hasExtension(oidExtensionSubjectAltName) {
ext, err := createSubjectAltNameExtension(&cert, cert.Subject.IsEmpty())
ext, err := createCertificateSubjectAltNameExtension(cert, cert.Subject.IsEmpty())
if err != nil {
return nil, err
}
Expand Down
37 changes: 37 additions & 0 deletions x509util/certificate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ func NewCertificateRequest(signer crypto.Signer, opts ...Option) (*CertificateRe
}
cr.PublicKey = pub
cr.Signer = signer

// Generate the subjectAltName extension if the certificate contains SANs
// that are not supported in the Go standard library.
if cr.hasExtendedSANs() && !cr.hasExtension(oidExtensionSubjectAltName) {
ext, err := createCertificateRequestSubjectAltNameExtension(cr, cr.Subject.IsEmpty())
if err != nil {
return nil, err
}
// Prepend extension to achieve a certificate as similar as possible to
// the one generated by the Go standard library.
cr.Extensions = append([]Extension{ext}, cr.Extensions...)
}

return &cr, nil
}

Expand Down Expand Up @@ -149,6 +162,30 @@ func (c *CertificateRequest) GetLeafCertificate() *Certificate {
return cert
}

// hasExtendedSANs returns true if the certificate contains any SAN types that
// are not supported by the golang x509 library (i.e. RegisteredID, OtherName,
// DirectoryName, X400Address, or EDIPartyName)
//
// See also https://datatracker.ietf.org/doc/html/rfc5280.html#section-4.2.1.6
func (c *CertificateRequest) hasExtendedSANs() bool {
for _, san := range c.SANs {
if !(san.Type == DNSType || san.Type == EmailType || san.Type == IPType || san.Type == URIType || san.Type == AutoType || san.Type == "") {
return true
}
}
return false
}

// hasExtension returns true if the given extension oid is in the certificate.
func (c *CertificateRequest) hasExtension(oid ObjectIdentifier) bool {
for _, e := range c.Extensions {
if e.ID.Equal(oid) {
return true
}
}
return false
}

// CreateCertificateRequest creates a simple X.509 certificate request with the
// given common name and sans.
func CreateCertificateRequest(commonName string, sans []string, signer crypto.Signer) (*x509.CertificateRequest, error) {
Expand Down
89 changes: 88 additions & 1 deletion x509util/certificate_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"fmt"
"net"
"net/url"
"reflect"
Expand All @@ -20,6 +22,49 @@ func TestNewCertificateRequest(t *testing.T) {
t.Fatal(err)
}

// ok extended sans
sans := []SubjectAlternativeName{
{Type: DNSType, Value: "foo.com"},
{Type: EmailType, Value: "root@foo.com"},
{Type: IPType, Value: "3.14.15.92"},
{Type: URIType, Value: "mailto:root@foo.com"},
{Type: PermanentIdentifierType, Value: "123456789"},
}
extendedSANs := CreateTemplateData("123456789", nil)
extendedSANs.SetSubjectAlternativeNames(sans...)
extendedSANsExtension, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, false)
if err != nil {
t.Fatal(err)
}

// ok extended sans and extension
extendedSANsAndExtensionsTemplate := fmt.Sprintf(`{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
"extensions": [
{"id":"2.5.29.17", "value":"%s"}
]
}`, base64.StdEncoding.EncodeToString(extendedSANsExtension.Value))

// ok permanent identifier template
permanentIdentifierTemplate := `{
"subject": {{ toJson .Subject }},
"sans": [{
"type": "permanentIdentifier",
"value": {{ toJson .Subject.CommonName }}
}]
}`
permanentIdentifierTemplateExtension, err := createSubjectAltNameExtension(nil, nil, nil, nil, []SubjectAlternativeName{
{Type: PermanentIdentifierType, Value: "123456789"},
}, false)
if err != nil {
t.Fatal(err)
}

// fail extended sans
failExtendedSANs := CreateTemplateData("123456789", nil)
failExtendedSANs.SetSubjectAlternativeNames(SubjectAlternativeName{Type: "badType", Value: "foo.com"})

type args struct {
signer crypto.Signer
opts []Option
Expand Down Expand Up @@ -47,8 +92,50 @@ func TestNewCertificateRequest(t *testing.T) {
PublicKey: signer.Public(),
Signer: signer,
}, false},
{"ok extended sans", args{signer, []Option{
WithTemplate(DefaultCertificateRequestTemplate, extendedSANs),
}}, &CertificateRequest{
Subject: Subject{CommonName: "123456789"},
SANs: []SubjectAlternativeName{
{Type: "dns", Value: "foo.com"},
{Type: "email", Value: "root@foo.com"},
{Type: "ip", Value: "3.14.15.92"},
{Type: "uri", Value: "mailto:root@foo.com"},
{Type: "permanentIdentifier", Value: "123456789"},
},
Extensions: []Extension{extendedSANsExtension},
PublicKey: signer.Public(),
Signer: signer,
}, false},
{"ok extended sans and extension", args{signer, []Option{
WithTemplate(extendedSANsAndExtensionsTemplate, extendedSANs),
}}, &CertificateRequest{
Subject: Subject{CommonName: "123456789"},
SANs: []SubjectAlternativeName{
{Type: "dns", Value: "foo.com"},
{Type: "email", Value: "root@foo.com"},
{Type: "ip", Value: "3.14.15.92"},
{Type: "uri", Value: "mailto:root@foo.com"},
{Type: "permanentIdentifier", Value: "123456789"},
},
Extensions: []Extension{extendedSANsExtension},
PublicKey: signer.Public(),
Signer: signer,
}, false},
{"ok permanent identifier template", args{signer, []Option{
WithTemplate(permanentIdentifierTemplate, CreateTemplateData("123456789", []string{})),
}}, &CertificateRequest{
Subject: Subject{CommonName: "123456789"},
SANs: []SubjectAlternativeName{
{Type: "permanentIdentifier", Value: "123456789"},
},
Extensions: []Extension{permanentIdentifierTemplateExtension},
PublicKey: signer.Public(),
Signer: signer,
}, false},
{"fail apply", args{signer, []Option{WithTemplateFile("testdata/missing.tpl", NewTemplateData())}}, nil, true},
{"fail unmarshal", args{signer, []Option{WithTemplate("{badjson", NewTemplateData())}}, nil, true},
{"fail extended sans", args{signer, []Option{WithTemplate(DefaultCertificateRequestTemplate, failExtendedSANs)}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -58,7 +145,7 @@ func TestNewCertificateRequest(t *testing.T) {
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewCertificateRequest() = %v, want %v", got, tt.want)
t.Errorf("NewCertificateRequest() = %+v, want %v", got, tt.want)
}
})
}
Expand Down
24 changes: 16 additions & 8 deletions x509util/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,14 @@ func (s *SerialNumber) UnmarshalJSON(data []byte) error {
return nil
}

func createCertificateSubjectAltNameExtension(c Certificate, subjectIsEmpty bool) (Extension, error) {
return createSubjectAltNameExtension(c.DNSNames, c.EmailAddresses, c.IPAddresses, c.URIs, c.SANs, subjectIsEmpty)
}

func createCertificateRequestSubjectAltNameExtension(c CertificateRequest, subjectIsEmpty bool) (Extension, error) {
return createSubjectAltNameExtension(c.DNSNames, c.EmailAddresses, c.IPAddresses, c.URIs, c.SANs, subjectIsEmpty)
}

// createSubjectAltNameExtension will construct an Extension containing all
// SubjectAlternativeNames held in a Certificate. It implements more types than
// the golang x509 library, so it is used whenever OtherName or RegisteredID
Expand All @@ -931,11 +939,11 @@ func (s *SerialNumber) UnmarshalJSON(data []byte) error {
//
// TODO(mariano,unreality): X400Address, DirectoryName, and EDIPartyName types
// are defined in RFC5280 but are currently unimplemented
func createSubjectAltNameExtension(c *Certificate, subjectIsEmpty bool) (Extension, error) {
func createSubjectAltNameExtension(dnsNames, emailAddresses MultiString, ipAddresses MultiIP, uris MultiURL, sans []SubjectAlternativeName, subjectIsEmpty bool) (Extension, error) {
var zero Extension

var rawValues []asn1.RawValue
for _, dnsName := range c.DNSNames {
for _, dnsName := range dnsNames {
rawValue, err := SubjectAlternativeName{
Type: DNSType, Value: dnsName,
}.RawValue()
Expand All @@ -946,7 +954,7 @@ func createSubjectAltNameExtension(c *Certificate, subjectIsEmpty bool) (Extensi
rawValues = append(rawValues, rawValue)
}

for _, emailAddress := range c.EmailAddresses {
for _, emailAddress := range emailAddresses {
rawValue, err := SubjectAlternativeName{
Type: EmailType, Value: emailAddress,
}.RawValue()
Expand All @@ -957,9 +965,9 @@ func createSubjectAltNameExtension(c *Certificate, subjectIsEmpty bool) (Extensi
rawValues = append(rawValues, rawValue)
}

for _, uri := range c.URIs {
for _, ip := range ipAddresses {
rawValue, err := SubjectAlternativeName{
Type: URIType, Value: uri.String(),
Type: IPType, Value: ip.String(),
}.RawValue()
if err != nil {
return zero, err
Expand All @@ -968,9 +976,9 @@ func createSubjectAltNameExtension(c *Certificate, subjectIsEmpty bool) (Extensi
rawValues = append(rawValues, rawValue)
}

for _, ip := range c.IPAddresses {
for _, uri := range uris {
rawValue, err := SubjectAlternativeName{
Type: IPType, Value: ip.String(),
Type: URIType, Value: uri.String(),
}.RawValue()
if err != nil {
return zero, err
Expand All @@ -979,7 +987,7 @@ func createSubjectAltNameExtension(c *Certificate, subjectIsEmpty bool) (Extensi
rawValues = append(rawValues, rawValue)
}

for _, san := range c.SANs {
for _, san := range sans {
rawValue, err := san.RawValue()
if err != nil {
return zero, err
Expand Down
Loading

0 comments on commit 9215f10

Please sign in to comment.