diff --git a/x509util/certificate_request.go b/x509util/certificate_request.go index f0ecbf07..4af5ec70 100644 --- a/x509util/certificate_request.go +++ b/x509util/certificate_request.go @@ -8,8 +8,9 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/json" + "errors" + "fmt" - "github.com/pkg/errors" "go.step.sm/crypto/internal/utils" "golang.org/x/crypto/cryptobyte" cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" @@ -53,6 +54,10 @@ type CertificateRequest struct { URIs MultiURL `json:"uris"` SANs []SubjectAlternativeName `json:"sans"` Extensions []Extension `json:"extensions"` + KeyUsage KeyUsage `json:"keyUsage"` + ExtKeyUsage ExtKeyUsage `json:"extKeyUsage"` + UnknownExtKeyUsage UnknownExtKeyUsage `json:"unknownExtKeyUsage"` + BasicConstraints *BasicConstraints `json:"basicConstraints"` SignatureAlgorithm SignatureAlgorithm `json:"signatureAlgorithm"` ChallengePassword string `json:"-"` PublicKey interface{} `json:"-"` @@ -83,7 +88,7 @@ func NewCertificateRequest(signer crypto.Signer, opts ...Option) (*CertificateRe // With templates var cr CertificateRequest if err := json.NewDecoder(o.CertBuffer).Decode(&cr); err != nil { - return nil, errors.Wrap(err, "error unmarshaling certificate") + return nil, fmt.Errorf("error unmarshaling certificate: %w", err) } cr.PublicKey = pub cr.Signer = signer @@ -100,6 +105,33 @@ func NewCertificateRequest(signer crypto.Signer, opts ...Option) (*CertificateRe cr.Extensions = append([]Extension{ext}, cr.Extensions...) } + // Add KeyUsage extension if necessary. + if cr.KeyUsage != 0 && !cr.hasExtension(oidExtensionKeyUsage) { + ext, err := cr.KeyUsage.Extension() + if err != nil { + return nil, err + } + cr.Extensions = append([]Extension{ext}, cr.Extensions...) + } + + // Add ExtKeyUsage extension if necessary. + if len(cr.ExtKeyUsage) > 0 || len(cr.UnknownExtKeyUsage) > 0 { + ext, err := cr.ExtKeyUsage.Extension(cr.UnknownExtKeyUsage) + if err != nil { + return nil, err + } + cr.Extensions = append([]Extension{ext}, cr.Extensions...) + } + + // Add BasicConstraints extension if necessary. + if cr.BasicConstraints != nil { + ext, err := cr.BasicConstraints.Extension() + if err != nil { + return nil, err + } + cr.Extensions = append([]Extension{ext}, cr.Extensions...) + } + return &cr, nil } @@ -114,6 +146,12 @@ func NewCertificateRequest(signer crypto.Signer, opts ...Option) (*CertificateRe func NewCertificateRequestFromX509(cr *x509.CertificateRequest) *CertificateRequest { // Set SubjectAltName extension as critical if Subject is empty. fixSubjectAltName(cr) + // Extracts key usage, extended key usage, and basic constraints from the + // certificate extensions. For backward compatibility, this method does not + // return an error if an extension is improperly encoded or cannot be + // decoded. In such cases, the extension is simply ignored. + parsed, _ := parseCertificateRequestExtensions(cr.Extensions) + return &CertificateRequest{ Version: cr.Version, Subject: newSubject(cr.Subject), @@ -123,6 +161,10 @@ func NewCertificateRequestFromX509(cr *x509.CertificateRequest) *CertificateRequ IPAddresses: cr.IPAddresses, URIs: cr.URIs, Extensions: newExtensions(cr.Extensions), + KeyUsage: parsed.KeyUsage, + ExtKeyUsage: parsed.ExtKeyUsage, + UnknownExtKeyUsage: parsed.UnknownExtKeyUsage, + BasicConstraints: parsed.BasicConstraints, PublicKey: cr.PublicKey, PublicKeyAlgorithm: cr.PublicKeyAlgorithm, Signature: cr.Signature, @@ -146,7 +188,7 @@ func (c *CertificateRequest) GetCertificateRequest() (*x509.CertificateRequest, SignatureAlgorithm: x509.SignatureAlgorithm(c.SignatureAlgorithm), }, c.Signer) if err != nil { - return nil, errors.Wrap(err, "error creating certificate request") + return nil, fmt.Errorf("error creating certificate request: %w", err) } // If a challenge password is provided, encode and prepend it as a challenge @@ -193,7 +235,7 @@ func (c *CertificateRequest) addChallengePassword(asn1Data []byte) ([]byte, erro b, err := builder.Bytes() if err != nil { - return nil, errors.Wrap(err, "error marshaling challenge password") + return nil, fmt.Errorf("error marshaling challenge password: %w", err) } challengePasswordAttr := asn1.RawValue{ FullBytes: b, @@ -223,7 +265,7 @@ func (c *CertificateRequest) addChallengePassword(asn1Data []byte) ([]byte, erro // Marshal tbsCertificateRequest tbsCSRContents, err := asn1.Marshal(tbsCSR) if err != nil { - return nil, errors.Wrap(err, "error creating certificate request") + return nil, fmt.Errorf("error creating certificate request: %w", err) } tbsCSR.Raw = tbsCSRContents @@ -239,7 +281,7 @@ func (c *CertificateRequest) addChallengePassword(asn1Data []byte) ([]byte, erro } } if !found { - return nil, errors.Errorf("error creating certificate request: unsupported signature algorithm %s", sigAlgoOID) + return nil, fmt.Errorf("error creating certificate request: unsupported signature algorithm %q", sigAlgoOID) } // Sign tbsCertificateRequest @@ -253,7 +295,7 @@ func (c *CertificateRequest) addChallengePassword(asn1Data []byte) ([]byte, erro var signature []byte signature, err = c.Signer.Sign(rand.Reader, signed, hashFunc) if err != nil { - return nil, errors.Wrap(err, "error creating certificate request") + return nil, fmt.Errorf("error creating certificate request: %w", err) } // Build new certificate request and marshal @@ -266,7 +308,7 @@ func (c *CertificateRequest) addChallengePassword(asn1Data []byte) ([]byte, erro }, }) if err != nil { - return nil, errors.Wrap(err, "error creating certificate request") + return nil, fmt.Errorf("error creating certificate request: %w", err) } return asn1Data, nil } @@ -351,7 +393,7 @@ func CreateCertificateRequest(commonName string, sans []string, signer crypto.Si URIs: uris, }, signer) if err != nil { - return nil, errors.Wrap(err, "error creating certificate request") + return nil, fmt.Errorf("error creating certificate request: %w", err) } // This should not fail return x509.ParseCertificateRequest(asn1Data) @@ -368,3 +410,91 @@ func fixSubjectAltName(cr *x509.CertificateRequest) { } } } + +type certificateRequestParsedExtensions struct { + KeyUsage KeyUsage + ExtKeyUsage ExtKeyUsage + UnknownExtKeyUsage UnknownExtKeyUsage + BasicConstraints *BasicConstraints +} + +func parseCertificateRequestExtensions(exts []pkix.Extension) (cr certificateRequestParsedExtensions, errs error) { + var err error + for _, ext := range exts { + switch { + case ext.Id.Equal(oidExtensionKeyUsage): + if cr.KeyUsage, err = parseKeyUsageExtension(ext.Value); err != nil { + errs = errors.Join(errs, err) + } + case ext.Id.Equal(oidExtensionExtendedKeyUsage): + if cr.ExtKeyUsage, cr.UnknownExtKeyUsage, err = parseExtKeyUsageExtension(ext.Value); err != nil { + errs = errors.Join(errs, err) + } + case ext.Id.Equal(oidExtensionBasicConstraints): + if cr.BasicConstraints, err = parseBasicConstraintsExtension(ext.Value); err != nil { + errs = errors.Join(errs, err) + } + } + } + + return +} + +func parseKeyUsageExtension(der cryptobyte.String) (KeyUsage, error) { + var usageBits asn1.BitString + if !der.ReadASN1BitString(&usageBits) { + return 0, errors.New("invalid key usage") + } + + var usage int + for i := 0; i < 9; i++ { + if usageBits.At(i) != 0 { + usage |= 1 << uint(i) + } + } + + return KeyUsage(usage), nil +} + +func parseExtKeyUsageExtension(der cryptobyte.String) (ExtKeyUsage, UnknownExtKeyUsage, error) { + var extKeyUsages ExtKeyUsage + var unknownUsages UnknownExtKeyUsage + if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) { + return nil, nil, errors.New("invalid extended key usages") + } + for !der.Empty() { + var eku asn1.ObjectIdentifier + if !der.ReadASN1ObjectIdentifier(&eku) { + return nil, nil, errors.New("invalid extended key usages") + } + if extKeyUsage, ok := extKeyUsageFromOID(eku); ok { + extKeyUsages = append(extKeyUsages, extKeyUsage) + } else { + unknownUsages = append(unknownUsages, eku) + } + } + + return extKeyUsages, unknownUsages, nil +} + +func parseBasicConstraintsExtension(der cryptobyte.String) (*BasicConstraints, error) { + var isCA bool + if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) { + return nil, errors.New("invalid basic constraints") + } + if der.PeekASN1Tag(cryptobyte_asn1.BOOLEAN) { + if !der.ReadASN1Boolean(&isCA) { + return nil, errors.New("invalid basic constraints") + } + } + maxPathLen := -1 + if der.PeekASN1Tag(cryptobyte_asn1.INTEGER) { + if !der.ReadASN1Integer(&maxPathLen) { + return nil, errors.New("invalid basic constraints") + } + } + + return &BasicConstraints{ + IsCA: isCA, MaxPathLen: maxPathLen, + }, nil +} diff --git a/x509util/certificate_request_test.go b/x509util/certificate_request_test.go index 8712d905..2beb6093 100644 --- a/x509util/certificate_request_test.go +++ b/x509util/certificate_request_test.go @@ -19,13 +19,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/cryptobyte" + cbasn1 "golang.org/x/crypto/cryptobyte/asn1" ) func TestNewCertificateRequest(t *testing.T) { _, signer, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // ok extended sans sans := []SubjectAlternativeName{ @@ -38,9 +38,7 @@ func TestNewCertificateRequest(t *testing.T) { extendedSANs := CreateTemplateData("123456789", nil) extendedSANs.SetSubjectAlternativeNames(sans...) extendedSANsExtension, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, false) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // ok extended sans and extension extendedSANsAndExtensionsTemplate := fmt.Sprintf(`{ @@ -62,9 +60,34 @@ func TestNewCertificateRequest(t *testing.T) { permanentIdentifierTemplateExtension, err := createSubjectAltNameExtension(nil, nil, nil, nil, []SubjectAlternativeName{ {Type: PermanentIdentifierType, Value: "123456789"}, }, false) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + + // ok with key usage and basic constraints + caTemplate := `{ + "subject": {{ toJson .Subject }}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } + }` + + // ok with key usage and extended key usage + leafTemplate := `{ + "subject": {{ toJson .Subject }}, + "sans": {{ toJson .SANs }}, + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["serverAuth", "clientAuth"] + }` + + caKeyUsageExtension, err := KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign).Extension() + require.NoError(t, err) + leafKeyUsageExtension, err := KeyUsage(x509.KeyUsageDigitalSignature).Extension() + require.NoError(t, err) + basicConstraintsExtension, err := BasicConstraints{IsCA: true, MaxPathLen: 0}.Extension() + require.NoError(t, err) + extKeyUsageExtension, err := ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}.Extension(nil) + require.NoError(t, err) // fail extended sans failExtendedSANs := CreateTemplateData("123456789", nil) @@ -138,6 +161,40 @@ func TestNewCertificateRequest(t *testing.T) { PublicKey: signer.Public(), Signer: signer, }, false}, + {"ok with key usage and basic constraints", args{signer, []Option{ + WithTemplate(caTemplate, extendedSANs), + }}, &CertificateRequest{ + Subject: Subject{CommonName: "123456789"}, + KeyUsage: KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), + BasicConstraints: &BasicConstraints{IsCA: true, MaxPathLen: 0}, + Extensions: []Extension{ + basicConstraintsExtension, + caKeyUsageExtension, + }, + PublicKey: signer.Public(), + Signer: signer, + }, false}, + {"ok with key usage and extended key usage", args{signer, []Option{ + WithTemplate(leafTemplate, 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"}, + }, + KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature), + ExtKeyUsage: ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + Extensions: []Extension{ + extKeyUsageExtension, + leafKeyUsageExtension, + extendedSANsExtension, + }, + 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}, @@ -157,6 +214,15 @@ func TestNewCertificateRequest(t *testing.T) { } func Test_newCertificateRequest(t *testing.T) { + ku, err := KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign).Extension() + require.NoError(t, err) + eku, err := ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageCodeSigning}.Extension( + UnknownExtKeyUsage{{1, 2, 4, 8}, {1, 3, 5, 9}}, + ) + require.NoError(t, err) + bc, err := BasicConstraints{IsCA: true, MaxPathLen: 0}.Extension() + require.NoError(t, err) + type args struct { cr *x509.CertificateRequest } @@ -179,6 +245,54 @@ func Test_newCertificateRequest(t *testing.T) { PublicKey: []byte("publicKey"), SignatureAlgorithm: SignatureAlgorithm(x509.UnknownSignatureAlgorithm), }}, + {"with known extensions", args{&x509.CertificateRequest{ + Extensions: []pkix.Extension{ + {Id: []int{1, 2, 3}, Critical: true, Value: []byte{3, 2, 1}}, + {Id: asn1.ObjectIdentifier(ku.ID), Critical: ku.Critical, Value: ku.Value}, + {Id: asn1.ObjectIdentifier(eku.ID), Critical: eku.Critical, Value: eku.Value}, + {Id: asn1.ObjectIdentifier(bc.ID), Critical: bc.Critical, Value: bc.Value}, + }, + Subject: pkix.Name{Province: []string{"CA"}, CommonName: "commonName"}, + DNSNames: []string{"foo"}, + PublicKey: []byte("publicKey"), + SignatureAlgorithm: x509.PureEd25519, + }}, &CertificateRequest{ + Extensions: []Extension{ + {ID: []int{1, 2, 3}, Critical: true, Value: []byte{3, 2, 1}}, + ku, eku, bc, + }, + Subject: Subject{Province: []string{"CA"}, CommonName: "commonName"}, + DNSNames: []string{"foo"}, + KeyUsage: KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), + ExtKeyUsage: ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageCodeSigning}, + UnknownExtKeyUsage: UnknownExtKeyUsage{{1, 2, 4, 8}, {1, 3, 5, 9}}, + BasicConstraints: &BasicConstraints{IsCA: true, MaxPathLen: 0}, + PublicKey: []byte("publicKey"), + SignatureAlgorithm: SignatureAlgorithm(x509.UnknownSignatureAlgorithm), + }}, + {"with ignored errors", args{&x509.CertificateRequest{ + Extensions: []pkix.Extension{ + {Id: []int{1, 2, 3}, Critical: true, Value: []byte{3, 2, 1}}, + {Id: asn1.ObjectIdentifier(ku.ID), Critical: ku.Critical, Value: []byte("garbage")}, + {Id: asn1.ObjectIdentifier(eku.ID), Critical: eku.Critical, Value: []byte("garbage")}, + {Id: asn1.ObjectIdentifier(bc.ID), Critical: bc.Critical, Value: []byte("garbage")}, + }, + Subject: pkix.Name{Province: []string{"CA"}, CommonName: "commonName"}, + DNSNames: []string{"foo"}, + PublicKey: []byte("publicKey"), + SignatureAlgorithm: x509.PureEd25519, + }}, &CertificateRequest{ + Extensions: []Extension{ + {ID: []int{1, 2, 3}, Critical: true, Value: []byte{3, 2, 1}}, + {ID: ku.ID, Critical: ku.Critical, Value: []byte("garbage")}, + {ID: eku.ID, Critical: eku.Critical, Value: []byte("garbage")}, + {ID: bc.ID, Critical: bc.Critical, Value: []byte("garbage")}, + }, + Subject: Subject{Province: []string{"CA"}, CommonName: "commonName"}, + DNSNames: []string{"foo"}, + PublicKey: []byte("publicKey"), + SignatureAlgorithm: SignatureAlgorithm(x509.UnknownSignatureAlgorithm), + }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -754,3 +868,212 @@ func TestCreateCertificateRequest(t *testing.T) { }) } } + +func TestSignCertificateRequestTemplates(t *testing.T) { + iss, issPriv := createIssuerCertificate(t, "issuer") + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + t.Run("sign ca certificate maxPathLen 1", func(t *testing.T) { + template := `{ + "subject": {{ toJson .Subject }}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + csr, err := NewCertificateRequest(priv, WithTemplate(template, TemplateData{SubjectKey: Subject{ + CommonName: "CA Intermediate MaxPathLen 1", + }})) + require.NoError(t, err) + + crt, err := CreateCertificate(csr.GetCertificate().GetCertificate(), iss, pub, issPriv) + require.NoError(t, err) + assert.Equal(t, "CA Intermediate MaxPathLen 1", crt.Subject.CommonName) + assert.Equal(t, "issuer", crt.Issuer.CommonName) + assert.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign, crt.KeyUsage) + assert.True(t, crt.BasicConstraintsValid) + assert.True(t, crt.IsCA) + assert.False(t, false, crt.MaxPathLenZero) + assert.Equal(t, 1, crt.MaxPathLen) + }) + + t.Run("sign ca certificate maxPathLen 0", func(t *testing.T) { + template := `{ + "subject": {{ toJson .Subject }}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } + }` + csr, err := NewCertificateRequest(priv, WithTemplate(template, TemplateData{SubjectKey: Subject{ + CommonName: "CA Intermediate MaxPathLen 0", + }})) + require.NoError(t, err) + + crt, err := CreateCertificate(csr.GetCertificate().GetCertificate(), iss, pub, issPriv) + require.NoError(t, err) + assert.Equal(t, "CA Intermediate MaxPathLen 0", crt.Subject.CommonName) + assert.Equal(t, "issuer", crt.Issuer.CommonName) + assert.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign, crt.KeyUsage) + assert.True(t, crt.BasicConstraintsValid) + assert.True(t, crt.IsCA) + assert.True(t, crt.MaxPathLenZero) + assert.Equal(t, 0, crt.MaxPathLen) + }) + + t.Run("sign leaf certificate", func(t *testing.T) { + template := `{ + "subject": {{ toJson .Subject }}, + "sans": {{ toJson .SANs }}, + {{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }} + "keyUsage": ["keyEncipherment", "digitalSignature"], + {{- else }} + "keyUsage": ["digitalSignature"], + {{- end }} + "extKeyUsage": ["serverAuth", "clientAuth"] + }` + + csr, err := NewCertificateRequest(priv, WithTemplate(template, CreateTemplateData("leaf", []string{"leaf.example.com"}))) + require.NoError(t, err) + + crt, err := CreateCertificate(csr.GetCertificate().GetCertificate(), iss, pub, issPriv) + require.NoError(t, err) + assert.Equal(t, "leaf", crt.Subject.CommonName) + assert.Equal(t, "issuer", crt.Issuer.CommonName) + assert.Equal(t, []string{"leaf.example.com"}, crt.DNSNames) + assert.Equal(t, x509.KeyUsageDigitalSignature, crt.KeyUsage) + assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, crt.ExtKeyUsage) + }) + +} + +func Test_parseKeyUsageExtension(t *testing.T) { + mustValue := func(ku KeyUsage) cryptobyte.String { + ext, err := ku.Extension() + require.NoError(t, err) + return ext.Value + } + + type args struct { + der cryptobyte.String + } + tests := []struct { + name string + args args + want KeyUsage + assertion assert.ErrorAssertionFunc + }{ + {"ok", args{mustValue(KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign))}, KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), assert.NoError}, + {"ok", args{mustValue(KeyUsage(x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature))}, KeyUsage(x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature), assert.NoError}, + {"fail", args{cryptobyte.String("garbage")}, KeyUsage(0), assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseKeyUsageExtension(tt.args.der) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_parseExtKeyUsageExtension(t *testing.T) { + mustValue := func(eku ExtKeyUsage, ueku UnknownExtKeyUsage) cryptobyte.String { + ext, err := eku.Extension(ueku) + require.NoError(t, err) + return ext.Value + } + + b64OID, err := asn1Encode("oid:1.2.3.4") + require.NoError(t, err) + + b64Int, err := asn1Encode("int:10") + require.NoError(t, err) + + b64Seq, err := asn1Sequence(b64OID, b64Int) + require.NoError(t, err) + + badParse, err := base64.StdEncoding.DecodeString(b64Seq) + require.NoError(t, err) + + type args struct { + der cryptobyte.String + } + tests := []struct { + name string + args args + want ExtKeyUsage + want1 UnknownExtKeyUsage + assertion assert.ErrorAssertionFunc + }{ + {"fail parse", args{cryptobyte.String(badParse)}, nil, nil, assert.Error}, + {"ok", args{mustValue(ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, nil)}, + ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, nil, assert.NoError}, + {"ok unhandled", args{mustValue(ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, UnknownExtKeyUsage{{1, 2, 3, 4}, {1, 4, 6, 8}})}, + ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, UnknownExtKeyUsage{{1, 2, 3, 4}, {1, 4, 6, 8}}, assert.NoError}, + {"ok unhandled only", args{mustValue(ExtKeyUsage{}, UnknownExtKeyUsage{{1, 2, 3, 4}, {1, 4, 6, 8}})}, + nil, UnknownExtKeyUsage{{1, 2, 3, 4}, {1, 4, 6, 8}}, assert.NoError}, + {"fail", args{cryptobyte.String("garbage")}, nil, nil, assert.Error}, + {"fail parse", args{cryptobyte.String(badParse)}, nil, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := parseExtKeyUsageExtension(tt.args.der) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.want1, got1) + }) + } +} + +func Test_parseBasicConstraintsExtension(t *testing.T) { + mustValue := func(bc BasicConstraints) cryptobyte.String { + ext, err := bc.Extension() + require.NoError(t, err) + return ext.Value + } + + var b1 cryptobyte.Builder + b1.AddASN1(cbasn1.SEQUENCE, func(child *cryptobyte.Builder) { + // Tag 1 boolean and garbage + child.AddBytes([]byte{1, 2, 3}) + }) + failParseBool, err := b1.Bytes() + require.NoError(t, err) + + var b2 cryptobyte.Builder + b2.AddASN1(cbasn1.SEQUENCE, func(child *cryptobyte.Builder) { + child.AddASN1Boolean(true) + // Tag 2 integer and nothing + child.AddBytes([]byte{2}) + }) + failParseInt, err := b2.Bytes() + require.NoError(t, err) + + type args struct { + der cryptobyte.String + } + tests := []struct { + name string + args args + want *BasicConstraints + assertion assert.ErrorAssertionFunc + }{ + {"ok 0", args{mustValue(BasicConstraints{IsCA: true, MaxPathLen: 0})}, &BasicConstraints{IsCA: true, MaxPathLen: 0}, assert.NoError}, + {"ok 1", args{mustValue(BasicConstraints{IsCA: true, MaxPathLen: 1})}, &BasicConstraints{IsCA: true, MaxPathLen: 1}, assert.NoError}, + {"ok -1", args{mustValue(BasicConstraints{IsCA: true, MaxPathLen: -1})}, &BasicConstraints{IsCA: true, MaxPathLen: -1}, assert.NoError}, + {"ok no ca", args{mustValue(BasicConstraints{IsCA: false, MaxPathLen: 0})}, &BasicConstraints{IsCA: false, MaxPathLen: -1}, assert.NoError}, + {"fail", args{cryptobyte.String("garbage")}, nil, assert.Error}, + {"fail parse bool", args{failParseBool}, nil, assert.Error}, + {"fail parse int", args{failParseInt}, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseBasicConstraintsExtension(tt.args.der) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/x509util/certificate_test.go b/x509util/certificate_test.go index 46492921..b775394f 100644 --- a/x509util/certificate_test.go +++ b/x509util/certificate_test.go @@ -96,8 +96,8 @@ func createIssuerCertificate(t *testing.T, commonName string) (*x509.Certificate NotAfter: now.Add(time.Hour), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, - MaxPathLen: 0, - MaxPathLenZero: true, + MaxPathLen: -1, + MaxPathLenZero: false, Issuer: pkix.Name{CommonName: "issuer"}, Subject: pkix.Name{CommonName: "issuer"}, SerialNumber: sn, diff --git a/x509util/extensions.go b/x509util/extensions.go index 4c92aa4a..57b37806 100644 --- a/x509util/extensions.go +++ b/x509util/extensions.go @@ -23,6 +23,12 @@ func convertName(s string) string { return strings.ReplaceAll(strings.ToLower(s), "_", "") } +var ( + oidExtensionKeyUsage = []int{2, 5, 29, 15} + oidExtensionExtendedKeyUsage = []int{2, 5, 29, 37} + oidExtensionBasicConstraints = []int{2, 5, 29, 19} +) + // Names used for key usages. const ( KeyUsageDigitalSignature = "digitalSignature" @@ -54,6 +60,74 @@ const ( ExtKeyUsageMicrosoftKernelCodeSigning = "microsoftKernelCodeSigning" ) +// RFC 5280, 4.2.1.12 Extended Key Usage +// +// anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 } +// +// id-kp OBJECT IDENTIFIER ::= { id-pkix 3 } +// +// id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 } +// id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } +// id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 } +// id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 } +// id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 } +// id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 } +var ( + oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} + oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} + oidExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} + oidExtKeyUsageCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3} + oidExtKeyUsageEmailProtection = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4} + oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5} + oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6} + oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7} + oidExtKeyUsageTimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} + oidExtKeyUsageOCSPSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9} + oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3} + oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1} + oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22} + oidExtKeyUsageMicrosoftKernelCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1} +) + +// extKeyUsageOIDs contains the mapping between an ExtKeyUsage and its OID. +var extKeyUsageOIDs = []struct { + extKeyUsage x509.ExtKeyUsage + oid asn1.ObjectIdentifier +}{ + {x509.ExtKeyUsageAny, oidExtKeyUsageAny}, + {x509.ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth}, + {x509.ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth}, + {x509.ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning}, + {x509.ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection}, + {x509.ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem}, + {x509.ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel}, + {x509.ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser}, + {x509.ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping}, + {x509.ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning}, + {x509.ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto}, + {x509.ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto}, + {x509.ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning}, + {x509.ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning}, +} + +func extKeyUsageFromOID(oid asn1.ObjectIdentifier) (eku x509.ExtKeyUsage, ok bool) { + for _, pair := range extKeyUsageOIDs { + if oid.Equal(pair.oid) { + return pair.extKeyUsage, true + } + } + return +} + +func oidFromExtKeyUsage(eku x509.ExtKeyUsage) (oid asn1.ObjectIdentifier, ok bool) { + for _, pair := range extKeyUsageOIDs { + if eku == pair.extKeyUsage { + return pair.oid, true + } + } + return +} + // Names used and SubjectAlternativeNames types. const ( AutoType = "auto" @@ -201,13 +275,15 @@ func newExtensions(extensions []pkix.Extension) []Extension { return ret } -// Set adds the extension to the given X509 certificate. +// Set adds a non empty extension to the given X509 certificate. func (e Extension) Set(c *x509.Certificate) { - c.ExtraExtensions = append(c.ExtraExtensions, pkix.Extension{ - Id: asn1.ObjectIdentifier(e.ID), - Critical: e.Critical, - Value: e.Value, - }) + if len(e.ID) > 0 { + c.ExtraExtensions = append(c.ExtraExtensions, pkix.Extension{ + Id: asn1.ObjectIdentifier(e.ID), + Critical: e.Critical, + Value: e.Value, + }) + } } // ObjectIdentifier represents a JSON strings that unmarshals into an ASN1 @@ -604,6 +680,38 @@ func (k KeyUsage) Set(c *x509.Certificate) { c.KeyUsage = x509.KeyUsage(k) } +// Extension marshals the key usage to an [Extension]. It will return an empty +// extension if key usages is empty. +func (k KeyUsage) Extension() (Extension, error) { + if k == 0 { + return Extension{}, nil + } + + var b [2]byte + b[0] = reverseBitsInAByte(byte(k)) + b[1] = reverseBitsInAByte(byte(k >> 8)) + + l := 1 + if b[1] != 0 { + l = 2 + } + + bitString := b[:l] + value, err := asn1.Marshal(asn1.BitString{ + Bytes: bitString, + BitLength: asn1BitLength(bitString), + }) + if err != nil { + return Extension{}, fmt.Errorf("error marshaling keyUsage extension to ASN1: %w", err) + } + + return Extension{ + ID: oidExtensionKeyUsage, + Critical: true, + Value: value, + }, nil +} + // UnmarshalJSON implements the json.Unmarshaler interface and coverts a string // or a list of strings into a key usage. func (k *KeyUsage) UnmarshalJSON(data []byte) error { @@ -692,6 +800,36 @@ func (k ExtKeyUsage) Set(c *x509.Certificate) { c.ExtKeyUsage = []x509.ExtKeyUsage(k) } +// Extension marshals the extended key usages to an [Extension]. It will return +// an empty extension if there are no extended key usages. +func (k ExtKeyUsage) Extension(unknownUsages UnknownExtKeyUsage) (Extension, error) { + size := len(k) + len(unknownUsages) + if size == 0 { + return Extension{}, nil + } + + oids := make([]asn1.ObjectIdentifier, size) + for i, u := range k { + if oid, ok := oidFromExtKeyUsage(u); ok { + oids[i] = oid + } else { + return Extension{}, errors.New("unknown extended key usage") + } + } + + copy(oids[len(k):], unknownUsages) + + value, err := asn1.Marshal(oids) + if err != nil { + return Extension{}, fmt.Errorf("error marshaling extKeyUsage extension to ASN1: %w", err) + } + + return Extension{ + ID: oidExtensionExtendedKeyUsage, + Value: value, + }, nil +} + // UnmarshalJSON implements the json.Unmarshaler interface and coverts a string // or a list of strings into a list of extended key usages. func (k *ExtKeyUsage) UnmarshalJSON(data []byte) error { @@ -929,8 +1067,8 @@ func (p PolicyIdentifiers) Set(c *x509.Certificate) { // self-issued intermediate CA certificates may follow in a valid certification // path. To do not impose a limit the MaxPathLen should be set to -1. type BasicConstraints struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` + IsCA bool `json:"isCA" asn1:"optional"` + MaxPathLen int `json:"maxPathLen" asn1:"optional,default:-1"` } // Set sets the basic constraints to the given certificate. @@ -955,6 +1093,25 @@ func (b BasicConstraints) Set(c *x509.Certificate) { } } +// Extension marshals the basic constraints to an [Extension]. +func (b BasicConstraints) Extension() (Extension, error) { + // When IsCA is false the MaxPathLen must be the default -1. + if !b.IsCA || b.MaxPathLen < 0 { + b.MaxPathLen = -1 + } + + value, err := asn1.Marshal(b) + if err != nil { + return Extension{}, fmt.Errorf("error marshaling basicConstraints extension to ASN1: %w", err) + } + + return Extension{ + ID: oidExtensionBasicConstraints, + Critical: true, + Value: value, + }, nil +} + // NameConstraints represents the X509 Name constraints extension and defines a // names space within which all subject names in subsequent certificates in a // certificate path must be located. The name constraints extension must be used @@ -1286,3 +1443,30 @@ func forEachSAN(extension []byte, callback func(ext asn1.RawValue) error) error return nil } + +func reverseBitsInAByte(in byte) byte { + b1 := in>>4 | in<<4 + b2 := b1>>2&0x33 | b1<<2&0xcc + b3 := b2>>1&0x55 | b2<<1&0xaa + return b3 +} + +// asn1BitLength returns the bit-length of bitString by considering the +// most-significant bit in a byte to be the "first" bit. This convention matches +// ASN.1, but differs from almost everything else. +func asn1BitLength(bitString []byte) int { + bitLen := len(bitString) * 8 + + for i := range bitString { + b := bitString[len(bitString)-i-1] + + for bit := uint(0); bit < 8; bit++ { + if (b>>bit)&1 == 1 { + return bitLen + } + bitLen-- + } + } + + return 0 +} diff --git a/x509util/extensions_test.go b/x509util/extensions_test.go index a28c9c63..49edd017 100644 --- a/x509util/extensions_test.go +++ b/x509util/extensions_test.go @@ -2,6 +2,8 @@ package x509util import ( "bytes" + "crypto/ed25519" + "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -1665,3 +1667,162 @@ func TestPolicyIdentifiers(t *testing.T) { }) } } + +func TestKeyUsage_Extension(t *testing.T) { + pub, _, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + iss, issPriv := createIssuerCertificate(t, "issuer") + mustExtension := func(ku x509.KeyUsage) Extension { + crt, err := CreateCertificate(&x509.Certificate{ + Subject: pkix.Name{CommonName: "leaf"}, + PublicKey: pub, + KeyUsage: ku, + }, iss, pub, issPriv) + require.NoError(t, err) + + for _, ext := range crt.Extensions { + if ext.Id.Equal(oidExtensionKeyUsage) { + return newExtension(ext) + } + } + return Extension{} + } + + all := x509.KeyUsageDigitalSignature | + x509.KeyUsageContentCommitment | + x509.KeyUsageKeyEncipherment | + x509.KeyUsageDataEncipherment | + x509.KeyUsageKeyAgreement | + x509.KeyUsageCertSign | + x509.KeyUsageCRLSign | + x509.KeyUsageEncipherOnly | + x509.KeyUsageDecipherOnly + + tests := []struct { + name string + k KeyUsage + want Extension + assertion assert.ErrorAssertionFunc + }{ + {"ca", KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), mustExtension(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), assert.NoError}, + {"leaf", KeyUsage(x509.KeyUsageDigitalSignature), mustExtension(x509.KeyUsageDigitalSignature), assert.NoError}, + {"all", KeyUsage(all), mustExtension(all), assert.NoError}, + {"empty", KeyUsage(0), Extension{}, assert.NoError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.k.Extension() + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExtKeyUsage_Extension(t *testing.T) { + pub, _, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + iss, issPriv := createIssuerCertificate(t, "issuer") + mustExtension := func(eku []x509.ExtKeyUsage, unhandledEKUs ...asn1.ObjectIdentifier) Extension { + crt, err := CreateCertificate(&x509.Certificate{ + Subject: pkix.Name{CommonName: "leaf"}, + PublicKey: pub, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: eku, + UnknownExtKeyUsage: unhandledEKUs, + }, iss, pub, issPriv) + require.NoError(t, err) + + for _, ext := range crt.Extensions { + if ext.Id.Equal(oidExtensionExtendedKeyUsage) { + return newExtension(ext) + } + } + return Extension{} + } + + type args struct { + unknownUsages UnknownExtKeyUsage + } + tests := []struct { + name string + k ExtKeyUsage + args args + want Extension + assertion assert.ErrorAssertionFunc + }{ + {"ok", ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, args{nil}, mustExtension([]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}), assert.NoError}, + {"ok unhandled", ExtKeyUsage{x509.ExtKeyUsageClientAuth}, args{[]asn1.ObjectIdentifier{ + {1, 2, 3, 4}, {1, 5, 6, 7}, + }}, mustExtension([]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, asn1.ObjectIdentifier{1, 2, 3, 4}, asn1.ObjectIdentifier{1, 5, 6, 7}), assert.NoError}, + {"ok unhandled only", ExtKeyUsage{}, args{[]asn1.ObjectIdentifier{ + {1, 2, 3, 4}, {1, 5, 6, 7}, + }}, mustExtension([]x509.ExtKeyUsage{}, asn1.ObjectIdentifier{1, 2, 3, 4}, asn1.ObjectIdentifier{1, 5, 6, 7}), assert.NoError}, + {"empty", ExtKeyUsage{}, args{UnknownExtKeyUsage{}}, Extension{}, assert.NoError}, + {"empty nil", ExtKeyUsage{}, args{nil}, Extension{}, assert.NoError}, + {"fail extKeyUsage", ExtKeyUsage{x509.ExtKeyUsage(100)}, args{nil}, Extension{}, assert.Error}, + {"fail unhandled", ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, args{[]asn1.ObjectIdentifier{{1, 2, 3, 4}, {5, 6, 7, 8}}}, Extension{}, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.k.Extension(tt.args.unknownUsages) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestBasicConstraints_Extension(t *testing.T) { + pub, _, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + iss, issPriv := createIssuerCertificate(t, "issuer") + mustExtension := func(isCA bool, maxPathLen int, maxPathLenZero bool) Extension { + crt, err := CreateCertificate(&x509.Certificate{ + Subject: pkix.Name{CommonName: "ca"}, + PublicKey: pub, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + IsCA: isCA, + MaxPathLen: maxPathLen, + MaxPathLenZero: maxPathLenZero, + BasicConstraintsValid: true, + }, iss, pub, issPriv) + require.NoError(t, err) + + for _, ext := range crt.Extensions { + if ext.Id.Equal(oidExtensionBasicConstraints) { + return newExtension(ext) + } + } + return Extension{} + } + + type fields struct { + IsCA bool + MaxPathLen int + } + tests := []struct { + name string + fields fields + want Extension + assertion assert.ErrorAssertionFunc + }{ + {"ca 1", fields{true, 1}, mustExtension(true, 1, false), assert.NoError}, + {"ca 0", fields{true, 0}, mustExtension(true, 0, true), assert.NoError}, + {"ca -1", fields{true, -1}, mustExtension(true, -1, false), assert.NoError}, + {"ca -2", fields{true, -2}, mustExtension(true, -1, false), assert.NoError}, + {"no ca", fields{false, 0}, mustExtension(false, 0, false), assert.NoError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := BasicConstraints{ + IsCA: tt.fields.IsCA, + MaxPathLen: tt.fields.MaxPathLen, + } + got, err := b.Extension() + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/x509util/templates_test.go b/x509util/templates_test.go index a3d1653d..0d6262ca 100644 --- a/x509util/templates_test.go +++ b/x509util/templates_test.go @@ -2,8 +2,12 @@ package x509util import ( "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestTemplateError_Error(t *testing.T) { @@ -349,11 +353,28 @@ func TestTemplateData_SetAuthorizationCertificateChain(t *testing.T) { } func TestTemplateData_SetCertificateRequest(t *testing.T) { + ku, err := KeyUsage(x509.KeyUsageDigitalSignature).Extension() + require.NoError(t, err) + eku, err := ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageCodeSigning}.Extension(nil) + require.NoError(t, err) + cr := &x509.CertificateRequest{ DNSNames: []string{"foo", "bar"}, + Extensions: []pkix.Extension{{ + Id: asn1.ObjectIdentifier(eku.ID), + Critical: eku.Critical, + Value: eku.Value, + }, { + Id: asn1.ObjectIdentifier(ku.ID), + Critical: ku.Critical, + Value: ku.Value, + }}, } cr1 := &CertificateRequest{ - DNSNames: []string{"foo", "bar"}, + DNSNames: []string{"foo", "bar"}, + KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature), + ExtKeyUsage: ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageCodeSigning}, + Extensions: []Extension{eku, ku}, } cr2 := &CertificateRequest{ EmailAddresses: []string{"foo@bar.com"},