package authority import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha1" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280 "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "errors" "fmt" "net/http" "reflect" "testing" "time" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/minica" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" sassert "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/cas/softcas" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" "github.com/smallstep/nosql/database" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} stepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...) ) const provisionerTypeJWK = 1 type stepProvisionerASN1 struct { Type int Name []byte CredentialID []byte } type certificateDurationEnforcer struct { NotBefore time.Time NotAfter time.Time } func (m *certificateDurationEnforcer) Enforce(cert *x509.Certificate) error { cert.NotBefore = m.NotBefore cert.NotAfter = m.NotAfter return nil } type certificateChainDB struct { db.MockAuthDB MStoreCertificateChain func(provisioner.Interface, ...*x509.Certificate) error } func (d *certificateChainDB) StoreCertificateChain(p provisioner.Interface, certs ...*x509.Certificate) error { return d.MStoreCertificateChain(p, certs...) } func getDefaultIssuer(a *Authority) *x509.Certificate { return a.x509CAService.(*softcas.SoftCAS).CertificateChain[len(a.x509CAService.(*softcas.SoftCAS).CertificateChain)-1] } func getDefaultSigner(a *Authority) crypto.Signer { return a.x509CAService.(*softcas.SoftCAS).Signer } func generateCertificate(t *testing.T, commonName string, sans []string, opts ...interface{}) *x509.Certificate { t.Helper() priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) cr, err := x509util.CreateCertificateRequest(commonName, sans, priv) require.NoError(t, err) template, err := x509util.NewCertificate(cr) require.NoError(t, err) cert := template.GetCertificate() for _, m := range opts { switch m := m.(type) { case provisioner.CertificateModifierFunc: err = m.Modify(cert, provisioner.SignOptions{}) require.NoError(t, err) case signerFunc: cert, err = m(cert, priv.Public()) require.NoError(t, err) default: require.Fail(t, "", "unknown type %T", m) } } return cert } func generateRootCertificate(t *testing.T) (*x509.Certificate, crypto.Signer) { t.Helper() priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) cr, err := x509util.CreateCertificateRequest("TestRootCA", nil, priv) require.NoError(t, err) data := x509util.CreateTemplateData("TestRootCA", nil) template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data)) require.NoError(t, err) cert := template.GetCertificate() cert, err = x509util.CreateCertificate(cert, cert, priv.Public(), priv) require.NoError(t, err) return cert, priv } func generateIntermidiateCertificate(t *testing.T, issuer *x509.Certificate, signer crypto.Signer) (*x509.Certificate, crypto.Signer) { t.Helper() priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) cr, err := x509util.CreateCertificateRequest("TestIntermediateCA", nil, priv) require.NoError(t, err) data := x509util.CreateTemplateData("TestIntermediateCA", nil) template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data)) require.NoError(t, err) cert := template.GetCertificate() cert, err = x509util.CreateCertificate(cert, issuer, priv.Public(), signer) require.NoError(t, err) return cert, priv } func withSubject(sub pkix.Name) provisioner.CertificateModifierFunc { return func(crt *x509.Certificate, _ provisioner.SignOptions) error { crt.Subject = sub return nil } } func withProvisionerOID(name, kid string) provisioner.CertificateModifierFunc { return func(crt *x509.Certificate, _ provisioner.SignOptions) error { b, err := asn1.Marshal(stepProvisionerASN1{ Type: provisionerTypeJWK, Name: []byte(name), CredentialID: []byte(kid), }) if err != nil { return err } crt.ExtraExtensions = append(crt.ExtraExtensions, pkix.Extension{ Id: stepOIDProvisioner, Critical: false, Value: b, }) return nil } } func withNotBeforeNotAfter(notBefore, notAfter time.Time) provisioner.CertificateModifierFunc { return func(crt *x509.Certificate, _ provisioner.SignOptions) error { crt.NotBefore = notBefore crt.NotAfter = notAfter return nil } } type signerFunc func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error) func withSigner(issuer *x509.Certificate, signer crypto.Signer) signerFunc { return func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error) { return x509util.CreateCertificate(crt, issuer, pub, signer) } } func getCSR(t *testing.T, priv interface{}, opts ...func(*x509.CertificateRequest)) *x509.CertificateRequest { _csr := &x509.CertificateRequest{ Subject: pkix.Name{CommonName: "smallstep test"}, DNSNames: []string{"test.smallstep.com"}, } for _, opt := range opts { opt(_csr) } csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv) require.NoError(t, err) csr, err := x509.ParseCertificateRequest(csrBytes) require.NoError(t, err) return csr } func setExtraExtsCSR(exts []pkix.Extension) func(*x509.CertificateRequest) { return func(csr *x509.CertificateRequest) { csr.ExtraExtensions = exts } } func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { b, err := x509.MarshalPKIXPublicKey(pub) if err != nil { return nil, fmt.Errorf("error marshaling public key: %w", err) } info := struct { Algorithm pkix.AlgorithmIdentifier SubjectPublicKey asn1.BitString }{} if _, err = asn1.Unmarshal(b, &info); err != nil { return nil, fmt.Errorf("error unmarshaling public key: %w", err) } //nolint:gosec // used to create the Subject Key Identifier by RFC 5280 hash := sha1.Sum(info.SubjectPublicKey.Bytes) return hash[:], nil } type basicConstraints struct { IsCA bool `asn1:"optional"` MaxPathLen int `asn1:"optional,default:-1"` } type testEnforcer struct { enforcer func(*x509.Certificate) error } func (e *testEnforcer) Enforce(cert *x509.Certificate) error { if e.enforcer != nil { return e.enforcer(cert) } return nil } func TestAuthority_SignWithContext(t *testing.T) { pub, priv, err := keyutil.GenerateDefaultKeyPair() require.NoError(t, err) a := testAuthority(t) require.NoError(t, err) a.config.AuthorityConfig.Template = &ASN1DN{ Country: "Tazmania", Organization: "Acme Co", Locality: "Landscapes", Province: "Sudden Cliffs", StreetAddress: "TNT", CommonName: "test.smallstep.com", } nb := time.Now() signOpts := provisioner.SignOptions{ NotBefore: provisioner.NewTimeDuration(nb), NotAfter: provisioner.NewTimeDuration(nb.Add(time.Minute * 5)), Backdate: 1 * time.Minute, } // Create a token to get test extra opts. p := a.config.AuthorityConfig.Provisioners[1].(*provisioner.JWK) key, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) require.NoError(t, err) token, err := generateToken("smallstep test", "step-cli", testAudiences.Sign[0], []string{"test.smallstep.com"}, time.Now(), key) require.NoError(t, err) ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) extraOpts, err := a.Authorize(ctx, token) require.NoError(t, err) type signTest struct { auth *Authority csr *x509.CertificateRequest signOpts provisioner.SignOptions extraOpts []provisioner.SignOption notBefore time.Time notAfter time.Time extensionsCount int err error code int } tests := map[string]func(*testing.T) *signTest{ "fail invalid signature": func(t *testing.T) *signTest { csr := getCSR(t, priv) csr.Signature = []byte("foo") return &signTest{ auth: a, csr: csr, extraOpts: extraOpts, signOpts: signOpts, err: errors.New("invalid certificate request"), code: http.StatusBadRequest, } }, "fail invalid extra option": func(t *testing.T) *signTest { csr := getCSR(t, priv) csr.Raw = []byte("foo") return &signTest{ auth: a, csr: csr, extraOpts: append(extraOpts, "42"), signOpts: signOpts, err: errors.New("authority.Sign; invalid extra option type string"), code: http.StatusInternalServerError, } }, "fail merge default ASN1DN": func(t *testing.T) *signTest { _a := testAuthority(t) _a.config.AuthorityConfig.Template = nil csr := getCSR(t, priv) return &signTest{ auth: _a, csr: csr, extraOpts: extraOpts, signOpts: signOpts, err: errors.New("default ASN1DN template cannot be nil"), code: http.StatusForbidden, } }, "fail create cert": func(t *testing.T) *signTest { _a := testAuthority(t) _a.x509CAService.(*softcas.SoftCAS).Signer = nil csr := getCSR(t, priv) return &signTest{ auth: _a, csr: csr, extraOpts: extraOpts, signOpts: signOpts, err: errors.New("authority.Sign; error creating certificate"), code: http.StatusInternalServerError, } }, "fail provisioner duration claim": func(t *testing.T) *signTest { csr := getCSR(t, priv) _signOpts := provisioner.SignOptions{ NotBefore: provisioner.NewTimeDuration(nb), NotAfter: provisioner.NewTimeDuration(nb.Add(time.Hour * 25)), } return &signTest{ auth: a, csr: csr, extraOpts: extraOpts, signOpts: _signOpts, err: errors.New("requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h1m0s"), code: http.StatusForbidden, } }, "fail validate sans when adding common name not in claims": func(t *testing.T) *signTest { csr := getCSR(t, priv, func(csr *x509.CertificateRequest) { csr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName) }) return &signTest{ auth: a, csr: csr, extraOpts: extraOpts, signOpts: signOpts, err: errors.New("certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]"), code: http.StatusForbidden, } }, "fail rsa key too short": func(t *testing.T) *signTest { shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST----- MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG 9w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4 k205rv5/YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ 7JMMNB43CSLB2oNuwJjLeAM/yBzlhRnvpKjrNSfSV+cH54FXdnbFbcTFMStnjqKG MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ//cXMFK1SP8ktRU N2++E8Ww0Tet9oyNRArqxxS/UyVio63D3wynzRAB25PFGpYG1cN4b81Gv/foFUT6 W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr ZYtQ9Ot36qc= -----END CERTIFICATE REQUEST-----` block, _ := pem.Decode([]byte(shortRSAKeyPEM)) require.NoError(t, err) csr, err := x509.ParseCertificateRequest(block.Bytes) require.NoError(t, err) return &signTest{ auth: a, csr: csr, extraOpts: extraOpts, signOpts: signOpts, err: errors.New("certificate request RSA key must be at least 2048 bits (256 bytes)"), code: http.StatusForbidden, } }, "fail store cert in db": func(t *testing.T) *signTest { csr := getCSR(t, priv) _a := testAuthority(t) _a.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { return errors.New("force") }, } return &signTest{ auth: _a, csr: csr, extraOpts: extraOpts, signOpts: signOpts, err: errors.New("authority.Sign; error storing certificate in db: force"), code: http.StatusInternalServerError, } }, "fail custom template": func(t *testing.T) *signTest { csr := getCSR(t, priv) testAuthority := testAuthority(t) p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") if !ok { t.Fatal("provisioner not found") } p.(*provisioner.JWK).Options = &provisioner.Options{ X509: &provisioner.X509Options{Template: `{{ fail "fail message" }}`}, } testExtraOpts, err := testAuthority.Authorize(ctx, token) require.NoError(t, err) testAuthority.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: testAuthority, csr: csr, extraOpts: testExtraOpts, signOpts: signOpts, err: errors.New("fail message"), code: http.StatusBadRequest, } }, "fail bad JSON syntax template file": func(t *testing.T) *signTest { csr := getCSR(t, priv) testAuthority := testAuthority(t) p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") if !ok { t.Fatal("provisioner not found") } p.(*provisioner.JWK).Options = &provisioner.Options{ X509: &provisioner.X509Options{ TemplateFile: "./testdata/templates/badjsonsyntax.tpl", }, } testExtraOpts, err := testAuthority.Authorize(ctx, token) require.NoError(t, err) testAuthority.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: testAuthority, csr: csr, extraOpts: testExtraOpts, signOpts: signOpts, err: errors.New("error applying certificate template: invalid character"), code: http.StatusInternalServerError, } }, "fail bad JSON value template file": func(t *testing.T) *signTest { csr := getCSR(t, priv) testAuthority := testAuthority(t) p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") if !ok { t.Fatal("provisioner not found") } p.(*provisioner.JWK).Options = &provisioner.Options{ X509: &provisioner.X509Options{ TemplateFile: "./testdata/templates/badjsonvalue.tpl", }, } testExtraOpts, err := testAuthority.Authorize(ctx, token) require.NoError(t, err) testAuthority.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: testAuthority, csr: csr, extraOpts: testExtraOpts, signOpts: signOpts, err: errors.New("error applying certificate template: cannot unmarshal"), code: http.StatusInternalServerError, } }, "fail with provisioner enforcer": func(t *testing.T) *signTest { csr := getCSR(t, priv) aa := testAuthority(t) aa.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: aa, csr: csr, extraOpts: append(extraOpts, &testEnforcer{ enforcer: func(crt *x509.Certificate) error { return fmt.Errorf("an error") }, }), signOpts: signOpts, err: errors.New("error creating certificate"), code: http.StatusForbidden, } }, "fail with custom enforcer": func(t *testing.T) *signTest { csr := getCSR(t, priv) aa := testAuthority(t, WithX509Enforcers(&testEnforcer{ enforcer: func(cert *x509.Certificate) error { return fmt.Errorf("an error") }, })) aa.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: aa, csr: csr, extraOpts: extraOpts, signOpts: signOpts, err: errors.New("error creating certificate"), code: http.StatusForbidden, } }, "fail with policy": func(t *testing.T) *signTest { csr := getCSR(t, priv) aa := testAuthority(t) aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template aa.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { fmt.Println(crt.Subject) sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } options := &policy.Options{ X509: &policy.X509PolicyOptions{ DeniedNames: &policy.X509NameOptions{ DNSDomains: []string{"test.smallstep.com"}, }, }, } engine, err := policy.New(options) require.NoError(t, err) aa.policyEngine = engine return &signTest{ auth: aa, csr: csr, extraOpts: extraOpts, signOpts: signOpts, notBefore: signOpts.NotBefore.Time().Truncate(time.Second), notAfter: signOpts.NotAfter.Time().Truncate(time.Second), extensionsCount: 6, err: errors.New("dns name \"test.smallstep.com\" not allowed"), code: http.StatusForbidden, } }, "fail enriching webhooks": func(t *testing.T) *signTest { csr := getCSR(t, priv) csr.Raw = []byte("foo") return &signTest{ auth: a, csr: csr, extensionsCount: 7, extraOpts: append(extraOpts, &mockWebhookController{ enrichErr: provisioner.ErrWebhookDenied, }), signOpts: signOpts, err: provisioner.ErrWebhookDenied, code: http.StatusForbidden, } }, "fail authorizing webhooks": func(t *testing.T) *signTest { csr := getCSR(t, priv) csr.Raw = []byte("foo") return &signTest{ auth: a, csr: csr, extensionsCount: 7, extraOpts: append(extraOpts, &mockWebhookController{ authorizeErr: provisioner.ErrWebhookDenied, }), signOpts: signOpts, err: provisioner.ErrWebhookDenied, code: http.StatusForbidden, } }, "ok": func(t *testing.T) *signTest { csr := getCSR(t, priv) _a := testAuthority(t) _a.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: a, csr: csr, extraOpts: extraOpts, signOpts: signOpts, notBefore: signOpts.NotBefore.Time().Truncate(time.Second), notAfter: signOpts.NotAfter.Time().Truncate(time.Second), extensionsCount: 6, } }, "ok with enforced modifier": func(t *testing.T) *signTest { bcExt := pkix.Extension{} bcExt.Id = asn1.ObjectIdentifier{2, 5, 29, 19} bcExt.Critical = false bcExt.Value, err = asn1.Marshal(basicConstraints{IsCA: true, MaxPathLen: 4}) require.NoError(t, err) csr := getCSR(t, priv, setExtraExtsCSR([]pkix.Extension{ bcExt, {Id: stepOIDProvisioner, Value: []byte("foo")}, {Id: []int{1, 1, 1}, Value: []byte("bar")}})) now := time.Now().UTC() //nolint:gocritic enforcedExtraOptions := append(extraOpts, &certificateDurationEnforcer{ NotBefore: now, NotAfter: now.Add(365 * 24 * time.Hour), }) _a := testAuthority(t) _a.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: a, csr: csr, extraOpts: enforcedExtraOptions, signOpts: signOpts, notBefore: now.Truncate(time.Second), notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second), extensionsCount: 6, } }, "ok with custom template": func(t *testing.T) *signTest { csr := getCSR(t, priv) testAuthority := testAuthority(t) testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") if !ok { t.Fatal("provisioner not found") } p.(*provisioner.JWK).Options = &provisioner.Options{ X509: &provisioner.X509Options{Template: `{ "subject": {{toJson .Subject}}, "dnsNames": {{ toJson .Insecure.CR.DNSNames }}, "keyUsage": ["digitalSignature"], "extKeyUsage": ["serverAuth","clientAuth"] }`}, } testExtraOpts, err := testAuthority.Authorize(ctx, token) require.NoError(t, err) testAuthority.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } return &signTest{ auth: testAuthority, csr: csr, extraOpts: testExtraOpts, signOpts: signOpts, notBefore: signOpts.NotBefore.Time().Truncate(time.Second), notAfter: signOpts.NotAfter.Time().Truncate(time.Second), extensionsCount: 6, } }, "ok with enriching webhook": func(t *testing.T) *signTest { csr := getCSR(t, priv) testAuthority := testAuthority(t) testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") if !ok { t.Fatal("provisioner not found") } p.(*provisioner.JWK).Options = &provisioner.Options{ X509: &provisioner.X509Options{Template: `{ "subject": {"commonName": {{ toJson .Webhooks.people.role }} }, "dnsNames": {{ toJson .Insecure.CR.DNSNames }}, "keyUsage": ["digitalSignature"], "extKeyUsage": ["serverAuth","clientAuth"] }`}, } testExtraOpts, err := testAuthority.Authorize(ctx, token) require.NoError(t, err) testAuthority.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } for i, o := range testExtraOpts { if wc, ok := o.(*provisioner.WebhookController); ok { testExtraOpts[i] = &mockWebhookController{ templateData: wc.TemplateData, respData: map[string]any{"people": map[string]any{"role": "smallstep test"}}, } } } return &signTest{ auth: testAuthority, csr: csr, extraOpts: testExtraOpts, signOpts: signOpts, notBefore: signOpts.NotBefore.Time().Truncate(time.Second), notAfter: signOpts.NotAfter.Time().Truncate(time.Second), extensionsCount: 6, } }, "ok/csr with no template critical SAN extension": func(t *testing.T) *signTest { csr := getCSR(t, priv, func(csr *x509.CertificateRequest) { csr.Subject = pkix.Name{} }, func(csr *x509.CertificateRequest) { csr.DNSNames = []string{"foo", "bar"} }) now := time.Now().UTC() enforcedExtraOptions := []provisioner.SignOption{&certificateDurationEnforcer{ NotBefore: now, NotAfter: now.Add(365 * 24 * time.Hour), }} _a := testAuthority(t) _a.config.AuthorityConfig.Template = &ASN1DN{} _a.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject, pkix.Name{}) return nil }, } return &signTest{ auth: _a, csr: csr, extraOpts: enforcedExtraOptions, signOpts: provisioner.SignOptions{}, notBefore: now.Truncate(time.Second), notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second), extensionsCount: 5, } }, "ok with custom enforcer": func(t *testing.T) *signTest { csr := getCSR(t, priv) aa := testAuthority(t, WithX509Enforcers(&testEnforcer{ enforcer: func(cert *x509.Certificate) error { cert.CRLDistributionPoints = []string{"http://ca.example.org/leaf.crl"} return nil }, })) aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template aa.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") sassert.Equals(t, crt.CRLDistributionPoints, []string{"http://ca.example.org/leaf.crl"}) return nil }, } return &signTest{ auth: aa, csr: csr, extraOpts: extraOpts, signOpts: signOpts, notBefore: signOpts.NotBefore.Time().Truncate(time.Second), notAfter: signOpts.NotAfter.Time().Truncate(time.Second), extensionsCount: 7, } }, "ok with policy": func(t *testing.T) *signTest { csr := getCSR(t, priv) aa := testAuthority(t) aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template aa.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { sassert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } options := &policy.Options{ X509: &policy.X509PolicyOptions{ AllowedNames: &policy.X509NameOptions{ CommonNames: []string{"smallstep test"}, DNSDomains: []string{"*.smallstep.com"}, }, }, } engine, err := policy.New(options) require.NoError(t, err) aa.policyEngine = engine return &signTest{ auth: aa, csr: csr, extraOpts: extraOpts, signOpts: signOpts, notBefore: signOpts.NotBefore.Time().Truncate(time.Second), notAfter: signOpts.NotAfter.Time().Truncate(time.Second), extensionsCount: 6, } }, "ok with attestation data": func(t *testing.T) *signTest { csr := getCSR(t, priv) aa := testAuthority(t) aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template aa.db = &certificateChainDB{ MStoreCertificateChain: func(prov provisioner.Interface, certs ...*x509.Certificate) error { p, ok := prov.(attProvisioner) if assert.True(t, ok) { sassert.Equals(t, &provisioner.AttestationData{ PermanentIdentifier: "1234567890", }, p.AttestationData()) } if assert.Len(t, certs, 2) { sassert.Equals(t, certs[0].Subject.CommonName, "smallstep test") sassert.Equals(t, certs[1].Subject.CommonName, "smallstep Intermediate CA") } return nil }, } return &signTest{ auth: aa, csr: csr, extraOpts: append(extraOpts, provisioner.AttestationData{ PermanentIdentifier: "1234567890", }), signOpts: signOpts, notBefore: signOpts.NotBefore.Time().Truncate(time.Second), notAfter: signOpts.NotAfter.Time().Truncate(time.Second), extensionsCount: 6, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) certChain, err := tc.auth.SignWithContext(context.Background(), tc.csr, tc.signOpts, tc.extraOpts...) if err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { assert.Nil(t, certChain) var sc render.StatusCodedError sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") sassert.Equals(t, sc.StatusCode(), tc.code) sassert.HasPrefix(t, err.Error(), tc.err.Error()) var ctxErr *errs.Error sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error") sassert.Equals(t, ctxErr.Details["csr"], tc.csr) sassert.Equals(t, ctxErr.Details["signOptions"], tc.signOpts) } } else { leaf := certChain[0] intermediate := certChain[1] if assert.Nil(t, tc.err) { sassert.Equals(t, leaf.NotBefore, tc.notBefore) sassert.Equals(t, leaf.NotAfter, tc.notAfter) tmplt := a.config.AuthorityConfig.Template if tc.csr.Subject.CommonName == "" { sassert.Equals(t, leaf.Subject, pkix.Name{}) } else { sassert.Equals(t, leaf.Subject.String(), pkix.Name{ Country: []string{tmplt.Country}, Organization: []string{tmplt.Organization}, Locality: []string{tmplt.Locality}, StreetAddress: []string{tmplt.StreetAddress}, Province: []string{tmplt.Province}, CommonName: "smallstep test", }.String()) sassert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"}) } sassert.Equals(t, leaf.Issuer, intermediate.Subject) sassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256) sassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA) sassert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) issuer := getDefaultIssuer(a) subjectKeyID, err := generateSubjectKeyID(pub) require.NoError(t, err) sassert.Equals(t, leaf.SubjectKeyId, subjectKeyID) sassert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId) // Verify Provisioner OID found := 0 for _, ext := range leaf.Extensions { switch { case ext.Id.Equal(stepOIDProvisioner): found++ val := stepProvisionerASN1{} _, err := asn1.Unmarshal(ext.Value, &val) require.NoError(t, err) sassert.Equals(t, val.Type, provisionerTypeJWK) sassert.Equals(t, val.Name, []byte(p.Name)) sassert.Equals(t, val.CredentialID, []byte(p.Key.KeyID)) // Basic Constraints case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 19})): val := basicConstraints{} _, err := asn1.Unmarshal(ext.Value, &val) require.NoError(t, err) assert.False(t, val.IsCA, false) sassert.Equals(t, val.MaxPathLen, 0) // SAN extension case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 17})): if tc.csr.Subject.CommonName == "" { // Empty CSR subject test does not use any provisioner extensions. // So provisioner ID ext will be missing. found = 1 } } } sassert.Equals(t, found, 1) realIntermediate, err := x509.ParseCertificate(issuer.Raw) require.NoError(t, err) sassert.Equals(t, intermediate, realIntermediate) assert.Len(t, leaf.Extensions, tc.extensionsCount) } } }) } } func TestAuthority_Renew(t *testing.T) { a := testAuthority(t) a.config.AuthorityConfig.Template = &ASN1DN{ Country: "Tazmania", Organization: "Acme Co", Locality: "Landscapes", Province: "Sudden Cliffs", StreetAddress: "TNT", CommonName: "renew", } now := time.Now().UTC() nb1 := now.Add(-time.Minute * 7) na1 := now.Add(time.Hour) so := &provisioner.SignOptions{ NotBefore: provisioner.NewTimeDuration(nb1), NotAfter: provisioner.NewTimeDuration(na1), } issuer := getDefaultIssuer(a) signer := getDefaultSigner(a) cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), withSigner(issuer, signer)) certExtraNames := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withSubject(pkix.Name{ CommonName: "renew", ExtraNames: []pkix.AttributeTypeAndValue{ {Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}, Value: "dc"}, }, }), withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), withSigner(issuer, signer)) certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID), withSigner(issuer, signer)) type renewTest struct { auth *Authority cert *x509.Certificate err error code int } tests := map[string]func() (*renewTest, error){ "fail/create-cert": func() (*renewTest, error) { _a := testAuthority(t) _a.x509CAService.(*softcas.SoftCAS).Signer = nil return &renewTest{ auth: _a, cert: cert, err: errors.New("error creating certificate"), code: http.StatusInternalServerError, }, nil }, "fail/unauthorized": func() (*renewTest, error) { return &renewTest{ cert: certNoRenew, err: errors.New("authority.authorizeRenew: renew is disabled for provisioner 'dev'"), code: http.StatusUnauthorized, }, nil }, "fail/WithAuthorizeRenewFunc": func() (*renewTest, error) { aa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error { return errs.Unauthorized("not authorized") })) aa.x509CAService = a.x509CAService aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template return &renewTest{ auth: aa, cert: cert, err: errors.New("authority.authorizeRenew: not authorized"), code: http.StatusUnauthorized, }, nil }, "ok": func() (*renewTest, error) { return &renewTest{ auth: a, cert: cert, }, nil }, "ok/WithExtraNames": func() (*renewTest, error) { return &renewTest{ auth: a, cert: certExtraNames, }, nil }, "ok/success-new-intermediate": func() (*renewTest, error) { rootCert, rootSigner := generateRootCertificate(t) intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner) _a := testAuthority(t) _a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert} _a.x509CAService.(*softcas.SoftCAS).Signer = intSigner return &renewTest{ auth: _a, cert: cert, }, nil }, "ok/WithAuthorizeRenewFunc": func() (*renewTest, error) { aa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error { return nil })) aa.x509CAService = a.x509CAService aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template return &renewTest{ auth: aa, cert: cert, }, nil }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc, err := genTestCase() require.NoError(t, err) var certChain []*x509.Certificate if tc.auth != nil { certChain, err = tc.auth.Renew(tc.cert) } else { certChain, err = a.Renew(tc.cert) } if err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { assert.Nil(t, certChain) var sc render.StatusCodedError sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") sassert.Equals(t, sc.StatusCode(), tc.code) sassert.HasPrefix(t, err.Error(), tc.err.Error()) var ctxErr *errs.Error sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error") sassert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String()) } } else { leaf := certChain[0] intermediate := certChain[1] if assert.Nil(t, tc.err) { sassert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore)) assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute))) assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute))) expiry := now.Add(time.Minute * 7) assert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute))) assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour))) tmplt := a.config.AuthorityConfig.Template sassert.Equals(t, leaf.RawSubject, tc.cert.RawSubject) sassert.Equals(t, leaf.Subject.Country, []string{tmplt.Country}) sassert.Equals(t, leaf.Subject.Organization, []string{tmplt.Organization}) sassert.Equals(t, leaf.Subject.Locality, []string{tmplt.Locality}) sassert.Equals(t, leaf.Subject.StreetAddress, []string{tmplt.StreetAddress}) sassert.Equals(t, leaf.Subject.Province, []string{tmplt.Province}) sassert.Equals(t, leaf.Subject.CommonName, tmplt.CommonName) sassert.Equals(t, leaf.Issuer, intermediate.Subject) sassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256) sassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA) sassert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) sassert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"}) subjectKeyID, err := generateSubjectKeyID(leaf.PublicKey) require.NoError(t, err) sassert.Equals(t, leaf.SubjectKeyId, subjectKeyID) // We did not change the intermediate before renewing. authIssuer := getDefaultIssuer(tc.auth) if issuer.SerialNumber == authIssuer.SerialNumber { sassert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier if ext1.Id.Equal(oidSubjectKeyIdentifier) { continue } found := false for _, ext2 := range leaf.Extensions { if reflect.DeepEqual(ext1, ext2) { found = true break } } if !found { t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String()) } } } else { // We did change the intermediate before renewing. sassert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier if ext1.Id.Equal(oidSubjectKeyIdentifier) { continue } // The authority key id extension should be different b/c the intermediates are different. if ext1.Id.Equal(oidAuthorityKeyIdentifier) { for _, ext2 := range leaf.Extensions { assert.False(t, reflect.DeepEqual(ext1, ext2)) } continue } found := false for _, ext2 := range leaf.Extensions { if reflect.DeepEqual(ext1, ext2) { found = true break } } if !found { t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String()) } } } realIntermediate, err := x509.ParseCertificate(authIssuer.Raw) require.NoError(t, err) sassert.Equals(t, intermediate, realIntermediate) } } }) } } func TestAuthority_Rekey(t *testing.T) { pub, _, err := keyutil.GenerateDefaultKeyPair() require.NoError(t, err) a := testAuthority(t) a.config.AuthorityConfig.Template = &ASN1DN{ Country: "Tazmania", Organization: "Acme Co", Locality: "Landscapes", Province: "Sudden Cliffs", StreetAddress: "TNT", CommonName: "renew", } now := time.Now().UTC() nb1 := now.Add(-time.Minute * 7) na1 := now.Add(time.Hour) so := &provisioner.SignOptions{ NotBefore: provisioner.NewTimeDuration(nb1), NotAfter: provisioner.NewTimeDuration(na1), } issuer := getDefaultIssuer(a) signer := getDefaultSigner(a) cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), withSigner(issuer, signer)) certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID), withSigner(issuer, signer)) type renewTest struct { auth *Authority cert *x509.Certificate pk crypto.PublicKey err error code int } tests := map[string]func() (*renewTest, error){ "fail/create-cert": func() (*renewTest, error) { _a := testAuthority(t) _a.x509CAService.(*softcas.SoftCAS).Signer = nil return &renewTest{ auth: _a, cert: cert, err: errors.New("error creating certificate"), code: http.StatusInternalServerError, }, nil }, "fail/unauthorized": func() (*renewTest, error) { return &renewTest{ cert: certNoRenew, err: errors.New("authority.authorizeRenew: renew is disabled for provisioner 'dev'"), code: http.StatusUnauthorized, }, nil }, "ok/renew": func() (*renewTest, error) { return &renewTest{ auth: a, cert: cert, }, nil }, "ok/rekey": func() (*renewTest, error) { return &renewTest{ auth: a, cert: cert, pk: pub, }, nil }, "ok/renew/success-new-intermediate": func() (*renewTest, error) { rootCert, rootSigner := generateRootCertificate(t) intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner) _a := testAuthority(t) _a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert} _a.x509CAService.(*softcas.SoftCAS).Signer = intSigner return &renewTest{ auth: _a, cert: cert, }, nil }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc, err := genTestCase() require.NoError(t, err) var certChain []*x509.Certificate if tc.auth != nil { certChain, err = tc.auth.Rekey(tc.cert, tc.pk) } else { certChain, err = a.Rekey(tc.cert, tc.pk) } if err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { assert.Nil(t, certChain) var sc render.StatusCodedError sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") sassert.Equals(t, sc.StatusCode(), tc.code) sassert.HasPrefix(t, err.Error(), tc.err.Error()) var ctxErr *errs.Error sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error") sassert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String()) } } else { leaf := certChain[0] intermediate := certChain[1] if assert.Nil(t, tc.err) { sassert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore)) assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute))) assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute))) expiry := now.Add(time.Minute * 7) assert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute))) assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour))) tmplt := a.config.AuthorityConfig.Template sassert.Equals(t, leaf.Subject.String(), pkix.Name{ Country: []string{tmplt.Country}, Organization: []string{tmplt.Organization}, Locality: []string{tmplt.Locality}, StreetAddress: []string{tmplt.StreetAddress}, Province: []string{tmplt.Province}, CommonName: tmplt.CommonName, }.String()) sassert.Equals(t, leaf.Issuer, intermediate.Subject) sassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256) sassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA) sassert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) sassert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"}) // Test Public Key and SubjectKeyId expectedPK := tc.pk if tc.pk == nil { expectedPK = cert.PublicKey } sassert.Equals(t, leaf.PublicKey, expectedPK) subjectKeyID, err := generateSubjectKeyID(expectedPK) require.NoError(t, err) sassert.Equals(t, leaf.SubjectKeyId, subjectKeyID) if tc.pk == nil { sassert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId) } // We did not change the intermediate before renewing. authIssuer := getDefaultIssuer(tc.auth) if issuer.SerialNumber == authIssuer.SerialNumber { sassert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier if ext1.Id.Equal(oidSubjectKeyIdentifier) { continue } found := false for _, ext2 := range leaf.Extensions { if reflect.DeepEqual(ext1, ext2) { found = true break } } if !found { t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String()) } } } else { // We did change the intermediate before renewing. sassert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier if ext1.Id.Equal(oidSubjectKeyIdentifier) { continue } // The authority key id extension should be different b/c the intermediates are different. if ext1.Id.Equal(oidAuthorityKeyIdentifier) { for _, ext2 := range leaf.Extensions { assert.False(t, reflect.DeepEqual(ext1, ext2)) } continue } found := false for _, ext2 := range leaf.Extensions { if reflect.DeepEqual(ext1, ext2) { found = true break } } if !found { t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String()) } } } realIntermediate, err := x509.ParseCertificate(authIssuer.Raw) require.NoError(t, err) sassert.Equals(t, intermediate, realIntermediate) } } }) } } func TestAuthority_GetTLSOptions(t *testing.T) { type renewTest struct { auth *Authority opts *TLSOptions } tests := map[string]func() (*renewTest, error){ "default": func() (*renewTest, error) { a := testAuthority(t) return &renewTest{auth: a, opts: &DefaultTLSOptions}, nil }, "non-default": func() (*renewTest, error) { a := testAuthority(t) a.config.TLS = &TLSOptions{ CipherSuites: CipherSuites{ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", }, MinVersion: 1.0, MaxVersion: 1.1, Renegotiation: true, } return &renewTest{auth: a, opts: a.config.TLS}, nil }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc, err := genTestCase() require.NoError(t, err) opts := tc.auth.GetTLSOptions() sassert.Equals(t, opts, tc.opts) }) } } func TestAuthority_Revoke(t *testing.T) { reasonCode := 2 reason := "bob was let go" validIssuer := "step-cli" validAudience := testAudiences.Revoke now := time.Now().UTC() jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) require.NoError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) require.NoError(t, err) a := testAuthority(t) tlsRevokeCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) type test struct { auth *Authority ctx context.Context opts *RevokeOptions err error code int checkErrDetails func(err *errs.Error) } tests := map[string]func() test{ "fail/token/authorizeRevoke error": func() test { return test{ auth: a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ OTT: "foo", Serial: "sn", ReasonCode: reasonCode, Reason: reason, }, err: errors.New("authority.Revoke; error parsing token"), code: http.StatusUnauthorized, } }, "fail/nil-db": func() test { cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() require.NoError(t, err) return test{ auth: a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, err: errors.New("authority.Revoke; no persistence layer configured"), code: http.StatusNotImplemented, checkErrDetails: func(err *errs.Error) { sassert.Equals(t, err.Details["token"], raw) sassert.Equals(t, err.Details["tokenID"], "44") sassert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") }, } }, "fail/db-revoke": func() test { _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, MGetCertificate: func(sn string) (*x509.Certificate, error) { return nil, errors.New("not found") }, Err: errors.New("force"), })) cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() require.NoError(t, err) return test{ auth: _a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, err: errors.New("authority.Revoke: force"), code: http.StatusInternalServerError, checkErrDetails: func(err *errs.Error) { sassert.Equals(t, err.Details["token"], raw) sassert.Equals(t, err.Details["tokenID"], "44") sassert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") }, } }, "fail/already-revoked": func() test { _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, MGetCertificate: func(sn string) (*x509.Certificate, error) { return nil, errors.New("not found") }, Err: db.ErrAlreadyExists, })) cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() require.NoError(t, err) return test{ auth: _a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, err: errors.New("certificate with serial number 'sn' is already revoked"), code: http.StatusBadRequest, checkErrDetails: func(err *errs.Error) { sassert.Equals(t, err.Details["token"], raw) sassert.Equals(t, err.Details["tokenID"], "44") sassert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") }, } }, "ok/token": func() test { _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, MGetCertificate: func(sn string) (*x509.Certificate, error) { return nil, errors.New("not found") }, })) cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() require.NoError(t, err) return test{ auth: _a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, } }, "ok/mTLS": func() test { _a := testAuthority(t, WithDatabase(&db.MockAuthDB{})) crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") require.NoError(t, err) return test{ auth: _a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ Crt: crt, Serial: "102012593071130646873265215610956555026", ReasonCode: reasonCode, Reason: reason, MTLS: true, }, } }, "ok/mTLS-no-provisioner": func() test { _a := testAuthority(t, WithDatabase(&db.MockAuthDB{})) crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") require.NoError(t, err) // Filter out provisioner extension. for i, ext := range crt.Extensions { if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}) { crt.Extensions = append(crt.Extensions[:i], crt.Extensions[i+1:]...) break } } return test{ auth: _a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ Crt: crt, Serial: "102012593071130646873265215610956555026", ReasonCode: reasonCode, Reason: reason, MTLS: true, }, } }, "ok/ACME": func() test { _a := testAuthority(t, WithDatabase(&db.MockAuthDB{})) crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") require.NoError(t, err) return test{ auth: _a, ctx: tlsRevokeCtx, opts: &RevokeOptions{ Crt: crt, Serial: "102012593071130646873265215610956555026", ReasonCode: reasonCode, Reason: reason, ACME: true, }, } }, "ok/ssh": func() test { a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MRevoke: func(rci *db.RevokedCertificateInfo) error { return errors.New("Revoke was called") }, MRevokeSSH: func(rci *db.RevokedCertificateInfo) error { return nil }, })) cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() require.NoError(t, err) return test{ auth: a, ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod), opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, } }, } for name, f := range tests { tc := f() t.Run(name, func(t *testing.T) { if err := tc.auth.Revoke(tc.ctx, tc.opts); err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { var sc render.StatusCodedError sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") sassert.Equals(t, sc.StatusCode(), tc.code) sassert.HasPrefix(t, err.Error(), tc.err.Error()) var ctxErr *errs.Error sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error") sassert.Equals(t, ctxErr.Details["serialNumber"], tc.opts.Serial) sassert.Equals(t, ctxErr.Details["reasonCode"], tc.opts.ReasonCode) sassert.Equals(t, ctxErr.Details["reason"], tc.opts.Reason) sassert.Equals(t, ctxErr.Details["MTLS"], tc.opts.MTLS) sassert.Equals(t, ctxErr.Details["context"], provisioner.RevokeMethod.String()) if tc.checkErrDetails != nil { tc.checkErrDetails(ctxErr) } } } else { assert.Nil(t, tc.err) } }) } } func TestAuthority_constraints(t *testing.T) { ca, err := minica.New( minica.WithIntermediateTemplate(`{ "subject": {{ toJson .Subject }}, "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, "maxPathLen": 0 }, "nameConstraints": { "critical": true, "permittedDNSDomains": ["internal.example.org"], "excludedDNSDomains": ["internal.example.com"], "permittedIPRanges": ["192.168.1.0/24", "192.168.2.1/32"], "excludedIPRanges": ["192.168.3.0/24", "192.168.4.0/28"], "permittedEmailAddresses": ["root@example.org", "example.org", ".acme.org"], "excludedEmailAddresses": ["root@example.com", "example.com", ".acme.com"], "permittedURIDomains": ["uuid.example.org", ".acme.org"], "excludedURIDomains": ["uuid.example.com", ".acme.com"] } }`), ) if err != nil { t.Fatal(err) } auth, err := NewEmbedded(WithX509RootCerts(ca.Root), WithX509Signer(ca.Intermediate, ca.Signer)) if err != nil { t.Fatal(err) } signer, err := keyutil.GenerateDefaultSigner() if err != nil { t.Fatal(err) } tests := []struct { name string sans []string wantErr bool }{ {"ok dns", []string{"internal.example.org", "host.internal.example.org"}, false}, {"ok ip", []string{"192.168.1.10", "192.168.2.1"}, false}, {"ok email", []string{"root@example.org", "info@example.org", "info@www.acme.org"}, false}, {"ok uri", []string{"https://uuid.example.org/b908d973-5167-4a62-abe3-6beda358d82a", "https://uuid.acme.org/1724aae1-1bb3-44fb-83c3-9a1a18df67c8"}, false}, {"fail permitted dns", []string{"internal.acme.org"}, true}, {"fail excluded dns", []string{"internal.example.com"}, true}, {"fail permitted ips", []string{"192.168.2.10"}, true}, {"fail excluded ips", []string{"192.168.3.1"}, true}, {"fail permitted emails", []string{"root@acme.org"}, true}, {"fail excluded emails", []string{"root@example.com"}, true}, {"fail permitted uris", []string{"https://acme.org/uuid/7848819c-9d0b-4e12-bbff-cd66079a3444"}, true}, {"fail excluded uris", []string{"https://uuid.example.com/d325eda7-6356-4d60-b8f6-3d64724afeb3"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { csr, err := x509util.CreateCertificateRequest(tt.sans[0], tt.sans, signer) if err != nil { t.Fatal(err) } cert, err := ca.SignCSR(csr) if err != nil { t.Fatal(err) } data := x509util.CreateTemplateData(tt.sans[0], tt.sans) templateOption, err := provisioner.TemplateOptions(nil, data) if err != nil { t.Fatal(err) } _, err = auth.SignWithContext(context.Background(), csr, provisioner.SignOptions{}, templateOption) if (err != nil) != tt.wantErr { t.Errorf("Authority.SignWithContext() error = %v, wantErr %v", err, tt.wantErr) } _, err = auth.Renew(cert) if (err != nil) != tt.wantErr { t.Errorf("Authority.Renew() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestAuthority_CRL(t *testing.T) { reasonCode := 2 reason := "bob was let go" validIssuer := "step-cli" validAudience := testAudiences.Revoke now := time.Now().UTC() jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) require.NoError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) require.NoError(t, err) crlCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) var crlStore db.CertificateRevocationListInfo var revokedList []db.RevokedCertificateInfo type test struct { auth *Authority ctx context.Context expected []string err error } tests := map[string]func() test{ "fail/empty-crl": func() test { a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, MGetCertificate: func(sn string) (*x509.Certificate, error) { return nil, errors.New("not found") }, MStoreCRL: func(i *db.CertificateRevocationListInfo) error { crlStore = *i return nil }, MGetCRL: func() (*db.CertificateRevocationListInfo, error) { return nil, database.ErrNotFound }, MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) { return &revokedList, nil }, MRevoke: func(rci *db.RevokedCertificateInfo) error { revokedList = append(revokedList, *rci) return nil }, })) a.config.CRL = &config.CRLConfig{ Enabled: true, } return test{ auth: a, ctx: crlCtx, expected: nil, err: errors.New("authority.GetCertificateRevocationList: not found"), } }, "ok/crl-full": func() test { a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, MGetCertificate: func(sn string) (*x509.Certificate, error) { return nil, errors.New("not found") }, MStoreCRL: func(i *db.CertificateRevocationListInfo) error { crlStore = *i return nil }, MGetCRL: func() (*db.CertificateRevocationListInfo, error) { return &crlStore, nil }, MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) { return &revokedList, nil }, MRevoke: func(rci *db.RevokedCertificateInfo) error { revokedList = append(revokedList, *rci) return nil }, })) a.config.CRL = &config.CRLConfig{ Enabled: true, GenerateOnRevoke: true, } var ex []string for i := 0; i < 100; i++ { sn := fmt.Sprintf("%v", i) cl := jose.Claims{ Subject: fmt.Sprintf("sn-%v", i), Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: sn, } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() require.NoError(t, err) err = a.Revoke(crlCtx, &RevokeOptions{ Serial: sn, ReasonCode: reasonCode, Reason: reason, OTT: raw, }) require.NoError(t, err) ex = append(ex, sn) } return test{ auth: a, ctx: crlCtx, expected: ex, } }, } for name, f := range tests { tc := f() t.Run(name, func(t *testing.T) { crlInfo, err := tc.auth.GetCertificateRevocationList() if tc.err != nil { assert.EqualError(t, err, tc.err.Error()) assert.Nil(t, crlInfo) return } crl, parseErr := x509.ParseRevocationList(crlInfo.Data) require.NoError(t, parseErr) var cmpList []string for _, c := range crl.RevokedCertificateEntries { cmpList = append(cmpList, c.SerialNumber.String()) } assert.Equal(t, tc.expected, cmpList) }) } }