package authority import ( "context" "crypto" "crypto/ed25519" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "errors" "fmt" "net/http" "reflect" "strconv" "testing" "time" "golang.org/x/crypto/ssh" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/randutil" "go.step.sm/crypto/x509util" "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" ) var testAudiences = provisioner.Audiences{ Sign: []string{"https://example.com/1.0/sign", "https://example.com/sign"}, Revoke: []string{"https://example.com/1.0/revoke", "https://example.com/revoke"}, SSHSign: []string{"https://example.com/1.0/ssh/sign"}, SSHRevoke: []string{"https://example.com/1.0/ssh/revoke"}, SSHRenew: []string{"https://example.com/1.0/ssh/renew"}, SSHRekey: []string{"https://example.com/1.0/ssh/rekey"}, } type tokOption func(*jose.SignerOptions) error func withSSHPOPFile(cert *ssh.Certificate) tokOption { return func(so *jose.SignerOptions) error { so.WithHeader("sshpop", base64.StdEncoding.EncodeToString(cert.Marshal())) return nil } } func generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) { so := new(jose.SignerOptions) so.WithType("JWT") so.WithHeader("kid", jwk.KeyID) for _, o := range tokOpts { if err := o(so); err != nil { return "", err } } sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so) if err != nil { return "", err } id, err := randutil.ASCII(64) if err != nil { return "", err } claims := struct { jose.Claims SANS []string `json:"sans"` }{ Claims: jose.Claims{ ID: id, Subject: sub, Issuer: iss, IssuedAt: jose.NewNumericDate(iat), NotBefore: jose.NewNumericDate(iat), Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)), Audience: []string{aud}, }, SANS: sans, } return jose.Signed(sig).Claims(claims).CompactSerialize() } func TestAuthority_authorizeToken(t *testing.T) { a := testAuthority(t) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) now := time.Now().UTC() validIssuer := "step-cli" validAudience := []string{"https://example.com/revoke"} type authorizeTest struct { auth *Authority token string err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", err: errors.New("error parsing token"), code: http.StatusUnauthorized, } }, "fail/prehistoric-token": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), IssuedAt: jose.NewNumericDate(now.Add(-time.Hour)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("token issued before the bootstrap of certificate authority"), code: http.StatusUnauthorized, } }, "fail/provisioner-not-found": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } _sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "foo")) assert.FatalError(t, err) raw, err := jose.Signed(_sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("provisioner not found or invalid audience (https://example.com/revoke)"), code: http.StatusUnauthorized, } }, "ok/simpledb": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, } }, "fail/simpledb/token-already-used": func(t *testing.T) *authorizeTest { _a := testAuthority(t) cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) _, err = _a.authorizeToken(context.Background(), raw) assert.FatalError(t, err) return &authorizeTest{ auth: _a, token: raw, err: errors.New("token already used"), code: http.StatusUnauthorized, } }, "ok/sha256": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, } }, "fail/sha256/token-already-used": func(t *testing.T) *authorizeTest { _a := testAuthority(t) cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) _, err = _a.authorizeToken(context.Background(), raw) assert.FatalError(t, err) return &authorizeTest{ auth: _a, token: raw, err: errors.New("token already used"), code: http.StatusUnauthorized, } }, "ok/mockNoSQLDB": func(t *testing.T) *authorizeTest { _a := testAuthority(t) _a.db = &db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, } cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: _a, token: raw, } }, "fail/mockNoSQLDB/error": func(t *testing.T) *authorizeTest { _a := testAuthority(t) _a.db = &db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return false, errors.New("force") }, } cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: _a, token: raw, err: errors.New("failed when attempting to store token: force"), code: http.StatusInternalServerError, } }, "fail/mockNoSQLDB/token-already-used": func(t *testing.T) *authorizeTest { _a := testAuthority(t) _a.db = &db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return false, nil }, } cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: _a, token: raw, err: errors.New("token already used"), code: http.StatusUnauthorized, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) p, err := tc.auth.authorizeToken(context.Background(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, p.GetID(), "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") } } }) } } func TestAuthority_authorizeRevoke(t *testing.T) { a := testAuthority(t) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) now := time.Now().UTC() validIssuer := "step-cli" validAudience := []string{"https://example.com/revoke"} type authorizeTest struct { auth *Authority token string err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/token/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", err: errors.New("authority.authorizeRevoke: error parsing token"), code: http.StatusUnauthorized, } }, "fail/token/invalid-subject": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("authority.authorizeRevoke: jwk.AuthorizeRevoke: jwk.authorizeToken; jwk token subject cannot be empty"), code: http.StatusUnauthorized, } }, "ok/token": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", 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() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) if err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { assert.Nil(t, tc.err) } }) } } func TestAuthority_authorizeSign(t *testing.T) { a := testAuthority(t) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) now := time.Now().UTC() validIssuer := "step-cli" validAudience := []string{"https://example.com/sign"} type authorizeTest struct { auth *Authority token string err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", err: errors.New("authority.authorizeSign: error parsing token"), code: http.StatusUnauthorized, } }, "fail/invalid-subject": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("authority.authorizeSign: jwk.AuthorizeSign: jwk.authorizeToken; jwk token subject cannot be empty"), code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", 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() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) got, err := tc.auth.authorizeSign(context.Background(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, 9, len(got)) // number of provisioner.SignOptions returned } } }) } } func TestAuthority_Authorize(t *testing.T) { a := testAuthority(t) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) now := time.Now().UTC() validIssuer := "step-cli" type authorizeTest struct { auth *Authority token string ctx context.Context err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "default-to-signMethod": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: context.Background(), err: errors.New("authority.Authorize: authority.authorizeSign: error parsing token"), code: http.StatusUnauthorized, } }, "fail/sign/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod), err: errors.New("authority.Authorize: authority.authorizeSign: error parsing token"), code: http.StatusUnauthorized, } }, "ok/sign": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: testAudiences.Sign, ID: "1", } token, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: token, ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod), } }, "fail/revoke/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod), err: errors.New("authority.Authorize: authority.authorizeRevoke: error parsing token"), code: http.StatusUnauthorized, } }, "ok/revoke": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: testAudiences.Revoke, ID: "2", } token, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: token, ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod), } }, "fail/sshSign/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod), err: errors.New("authority.Authorize: authority.authorizeSSHSign: error parsing token"), code: http.StatusUnauthorized, } }, "fail/sshSign/disabled": func(t *testing.T) *authorizeTest { _a := testAuthority(t) _a.sshCAHostCertSignKey = nil _a.sshCAUserCertSignKey = nil return &authorizeTest{ auth: _a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod), err: errors.New("authority.Authorize; ssh certificate flows are not enabled"), code: http.StatusNotImplemented, } }, "ok/sshSign": func(t *testing.T) *authorizeTest { raw, err := generateSimpleSSHUserToken(validIssuer, testAudiences.SSHSign[0], jwk) assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod), } }, "fail/sshRenew/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod), err: errors.New("authority.Authorize: authority.authorizeSSHRenew: error parsing token"), code: http.StatusUnauthorized, } }, "fail/sshRenew/disabled": func(t *testing.T) *authorizeTest { _a := testAuthority(t) _a.sshCAHostCertSignKey = nil _a.sshCAUserCertSignKey = nil return &authorizeTest{ auth: _a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod), err: errors.New("authority.Authorize; ssh certificate flows are not enabled"), code: http.StatusNotImplemented, } }, "ok/sshRenew": func(t *testing.T) *authorizeTest { key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") assert.FatalError(t, err) signer, ok := key.(crypto.Signer) assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") sshSigner, err := ssh.NewSignerFromSigner(signer) assert.FatalError(t, err) cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) assert.FatalError(t, err) p, ok := a.provisioners.Load("sshpop/sshpop") assert.Fatal(t, ok, "sshpop provisioner not found in test authority") tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0]+"#sshpop/sshpop", []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) assert.FatalError(t, err) return &authorizeTest{ auth: a, token: tok, ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod), } }, "fail/sshRevoke/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod), err: errors.New("authority.Authorize: authority.authorizeSSHRevoke: error parsing token"), code: http.StatusUnauthorized, } }, "ok/sshRevoke": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: testAudiences.SSHRevoke, ID: "3", } token, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: token, ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod), } }, "fail/sshRekey/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod), err: errors.New("authority.Authorize: authority.authorizeSSHRekey: error parsing token"), code: http.StatusUnauthorized, } }, "fail/sshRekey/disabled": func(t *testing.T) *authorizeTest { _a := testAuthority(t) _a.sshCAHostCertSignKey = nil _a.sshCAUserCertSignKey = nil return &authorizeTest{ auth: _a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod), err: errors.New("authority.Authorize; ssh certificate flows are not enabled"), code: http.StatusNotImplemented, } }, "ok/sshRekey": func(t *testing.T) *authorizeTest { key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") assert.FatalError(t, err) signer, ok := key.(crypto.Signer) assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") sshSigner, err := ssh.NewSignerFromSigner(signer) assert.FatalError(t, err) cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) assert.FatalError(t, err) p, ok := a.provisioners.Load("sshpop/sshpop") assert.Fatal(t, ok, "sshpop provisioner not found in test authority") tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0]+"#sshpop/sshpop", []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) assert.FatalError(t, err) return &authorizeTest{ auth: a, token: tok, ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod), } }, "fail/unexpected-method": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", ctx: provisioner.NewContextWithMethod(context.Background(), 15), err: errors.New("authority.Authorize; method 15 is not supported"), code: http.StatusInternalServerError, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) got, err := tc.auth.Authorize(tc.ctx, tc.token) if err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { assert.Nil(t, got) sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) ctxErr, ok := err.(*errs.Error) assert.Fatal(t, ok, "error is not of type *errs.Error") assert.Equals(t, ctxErr.Details["token"], tc.token) } } else { assert.Nil(t, tc.err) } }) } } func TestAuthority_authorizeRenew(t *testing.T) { fooCrt, err := pemutil.ReadCertificate("testdata/certs/foo.crt") fooCrt.NotAfter = time.Now().Add(time.Hour) assert.FatalError(t, err) renewDisabledCrt, err := pemutil.ReadCertificate("testdata/certs/renew-disabled.crt") assert.FatalError(t, err) otherCrt, err := pemutil.ReadCertificate("testdata/certs/provisioner-not-found.crt") assert.FatalError(t, err) type authorizeTest struct { auth *Authority cert *x509.Certificate err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/db.IsRevoked-error": func(t *testing.T) *authorizeTest { a := testAuthority(t) a.db = &db.MockAuthDB{ MIsRevoked: func(key string) (bool, error) { return false, errors.New("force") }, } return &authorizeTest{ auth: a, cert: fooCrt, err: errors.New("authority.authorizeRenew: force"), code: http.StatusInternalServerError, } }, "fail/revoked": func(t *testing.T) *authorizeTest { a := testAuthority(t) a.db = &db.MockAuthDB{ MIsRevoked: func(key string) (bool, error) { return true, nil }, } return &authorizeTest{ auth: a, cert: fooCrt, err: errors.New("authority.authorizeRenew: certificate has been revoked"), code: http.StatusUnauthorized, } }, "fail/load-provisioner": func(t *testing.T) *authorizeTest { a := testAuthority(t) a.db = &db.MockAuthDB{ MIsRevoked: func(key string) (bool, error) { return false, nil }, } return &authorizeTest{ auth: a, cert: otherCrt, err: errors.New("authority.authorizeRenew: provisioner not found"), code: http.StatusUnauthorized, } }, "fail/provisioner-authorize-renewal-fail": func(t *testing.T) *authorizeTest { a := testAuthority(t) a.db = &db.MockAuthDB{ MIsRevoked: func(key string) (bool, error) { return false, nil }, } return &authorizeTest{ auth: a, cert: renewDisabledCrt, err: errors.New("authority.authorizeRenew: renew is disabled for provisioner 'renew_disabled'"), code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { a := testAuthority(t) a.db = &db.MockAuthDB{ MIsRevoked: func(key string) (bool, error) { return false, nil }, } return &authorizeTest{ auth: a, cert: fooCrt, } }, "ok/from db": func(t *testing.T) *authorizeTest { a := testAuthority(t) a.db = &db.MockAuthDB{ MIsRevoked: func(key string) (bool, error) { return false, nil }, MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) { p, ok := a.provisioners.LoadByName("step-cli") if !ok { t.Fatal("provisioner step-cli not found") } return &db.CertificateData{ Provisioner: &db.ProvisionerData{ ID: p.GetID(), }, }, nil }, } return &authorizeTest{ auth: a, cert: fooCrt, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) err := tc.auth.authorizeRenew(tc.cert) if err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCoder interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) ctxErr, ok := err.(*errs.Error) assert.Fatal(t, ok, "error is not of type *errs.Error") assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String()) } } else { assert.Nil(t, tc.err) } }) } } func generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) { return generateSSHToken("subject@localhost", iss, aud, time.Now(), &provisioner.SignSSHOptions{ CertType: "user", Principals: []string{"name"}, }, jwk) } type stepPayload struct { SSH *provisioner.SignSSHOptions `json:"ssh,omitempty"` } func generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *provisioner.SignSSHOptions, jwk *jose.JSONWebKey) (string, error) { sig, err := jose.NewSigner( jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID), ) if err != nil { return "", err } id, err := randutil.ASCII(64) if err != nil { return "", err } claims := struct { jose.Claims Step *stepPayload `json:"step,omitempty"` }{ Claims: jose.Claims{ ID: id, Subject: sub, Issuer: iss, IssuedAt: jose.NewNumericDate(iat), NotBefore: jose.NewNumericDate(iat), Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)), Audience: []string{aud}, }, Step: &stepPayload{ SSH: sshOpts, }, } return jose.Signed(sig).Claims(claims).CompactSerialize() } func createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, *jose.JSONWebKey, error) { now := time.Now() jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "foo", 0) if err != nil { return nil, nil, err } cert.Key, err = ssh.NewPublicKey(jwk.Public().Key) if err != nil { return nil, nil, err } if cert.ValidAfter == 0 { cert.ValidAfter = uint64(now.Unix()) } if cert.ValidBefore == 0 { cert.ValidBefore = uint64(now.Add(time.Hour).Unix()) } if err := cert.SignCert(rand.Reader, signer); err != nil { return nil, nil, err } return cert, jwk, nil } func TestAuthority_authorizeSSHSign(t *testing.T) { a := testAuthority(t) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) now := time.Now().UTC() validIssuer := "step-cli" validAudience := []string{"https://example.com/ssh/sign"} type authorizeTest struct { auth *Authority token string err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", err: errors.New("authority.authorizeSSHSign: error parsing token"), code: http.StatusUnauthorized, } }, "fail/invalid-subject": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("authority.authorizeSSHSign: jwk.AuthorizeSSHSign: jwk.authorizeToken; jwk token subject cannot be empty"), code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { raw, err := generateSimpleSSHUserToken(validIssuer, validAudience[0], jwk) assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Len(t, 9, got) // number of provisioner.SignOptions returned } } }) } } func TestAuthority_authorizeSSHRenew(t *testing.T) { now := time.Now().UTC() sshpop := func(a *Authority) (*ssh.Certificate, string) { p, ok := a.provisioners.Load("sshpop/sshpop") assert.Fatal(t, ok, "sshpop provisioner not found in test authority") key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") assert.FatalError(t, err) signer, ok := key.(crypto.Signer) assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") sshSigner, err := ssh.NewSignerFromSigner(signer) assert.FatalError(t, err) cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) assert.FatalError(t, err) token, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0]+"#sshpop/sshpop", []string{"foo.smallstep.com"}, now, jwk, withSSHPOPFile(cert)) assert.FatalError(t, err) return cert, token } a := testAuthority(t) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) validIssuer := "step-cli" type authorizeTest struct { auth *Authority token string cert *ssh.Certificate err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", err: errors.New("authority.authorizeSSHRenew: error parsing token"), code: http.StatusUnauthorized, } }, "fail/sshRenew-unimplemented-jwk-provisioner": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: testAudiences.SSHRenew, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("authority.authorizeSSHRenew: provisioner.AuthorizeSSHRenew not implemented"), code: http.StatusUnauthorized, } }, "fail/WithAuthorizeSSHRenewFunc": func(t *testing.T) *authorizeTest { aa := testAuthority(t, WithAuthorizeSSHRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *ssh.Certificate) error { return errs.Forbidden("forbidden") })) _, token := sshpop(aa) return &authorizeTest{ auth: aa, token: token, err: errors.New("authority.authorizeSSHRenew: forbidden"), code: http.StatusForbidden, } }, "ok": func(t *testing.T) *authorizeTest { cert, token := sshpop(a) return &authorizeTest{ auth: a, token: token, cert: cert, } }, "ok/WithAuthorizeSSHRenewFunc": func(t *testing.T) *authorizeTest { aa := testAuthority(t, WithAuthorizeSSHRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *ssh.Certificate) error { return nil })) cert, token := sshpop(aa) return &authorizeTest{ auth: aa, token: token, cert: cert, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) got, err := tc.auth.authorizeSSHRenew(context.Background(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, tc.cert.Serial, got.Serial) } } }) } } func TestAuthority_authorizeSSHRevoke(t *testing.T) { a := testAuthority(t, []Option{WithDatabase(&db.MockAuthDB{ MIsSSHRevoked: func(serial string) (bool, error) { return false, nil }, MUseToken: func(id, tok string) (bool, error) { return true, nil }, })}...) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) now := time.Now().UTC() validIssuer := "step-cli" type authorizeTest struct { auth *Authority token string cert *ssh.Certificate err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", err: errors.New("authority.authorizeSSHRevoke: error parsing token"), code: http.StatusUnauthorized, } }, "fail/invalid-subject": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: testAudiences.SSHRevoke, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("authority.authorizeSSHRevoke: jwk.AuthorizeSSHRevoke: jwk.authorizeToken; jwk token subject cannot be empty"), code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") assert.FatalError(t, err) signer, ok := key.(crypto.Signer) assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") sshSigner, err := ssh.NewSignerFromSigner(signer) assert.FatalError(t, err) cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) assert.FatalError(t, err) p, ok := a.provisioners.Load("sshpop/sshpop") assert.Fatal(t, ok, "sshpop provisioner not found in test authority") tok, err := generateToken(strconv.FormatUint(cert.Serial, 10), p.GetName(), testAudiences.SSHRevoke[0]+"#sshpop/sshpop", []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) assert.FatalError(t, err) return &authorizeTest{ auth: a, token: tok, cert: cert, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) if err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { assert.Nil(t, tc.err) } }) } } func TestAuthority_authorizeSSHRekey(t *testing.T) { a := testAuthority(t) jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) now := time.Now().UTC() validIssuer := "step-cli" type authorizeTest struct { auth *Authority token string cert *ssh.Certificate err error code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ auth: a, token: "foo", err: errors.New("authority.authorizeSSHRekey: error parsing token"), code: http.StatusUnauthorized, } }, "fail/sshRekey-unimplemented-jwk-provisioner": func(t *testing.T) *authorizeTest { cl := jose.Claims{ Subject: "", Issuer: validIssuer, NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: testAudiences.SSHRekey, ID: "43", } raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ auth: a, token: raw, err: errors.New("authority.authorizeSSHRekey: provisioner.AuthorizeSSHRekey not implemented"), code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") assert.FatalError(t, err) signer, ok := key.(crypto.Signer) assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") sshSigner, err := ssh.NewSignerFromSigner(signer) assert.FatalError(t, err) cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) assert.FatalError(t, err) p, ok := a.provisioners.Load("sshpop/sshpop") assert.Fatal(t, ok, "sshpop provisioner not found in test authority") tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0]+"#sshpop/sshpop", []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) assert.FatalError(t, err) return &authorizeTest{ auth: a, token: tok, cert: cert, } }, } for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) cert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(render.StatusCodedError) assert.Fatal(t, ok, "error does not implement StatusCodedError interface") assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, tc.cert.Serial, cert.Serial) assert.Len(t, 4, signOpts) } } }) } } func TestAuthority_AuthorizeRenewToken(t *testing.T) { ctx := context.Background() type stepProvisionerASN1 struct { Type int Name []byte CredentialID []byte KeyValuePairs []string `asn1:"optional,omitempty"` } _, signer, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } csr, err := x509util.CreateCertificateRequest("test.example.com", []string{"test.example.com"}, signer) if err != nil { t.Fatal(err) } _, otherSigner, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } generateX5cToken := func(a *Authority, key crypto.Signer, claims jose.Claims, opts ...provisioner.SignOption) (string, *x509.Certificate) { chain, err := a.Sign(csr, provisioner.SignOptions{}, opts...) if err != nil { t.Fatal(err) } var x5c []string for _, c := range chain { x5c = append(x5c, base64.StdEncoding.EncodeToString(c.Raw)) } so := new(jose.SignerOptions) so.WithType("JWT") so.WithHeader("x5cInsecure", x5c) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.EdDSA, Key: key}, so) if err != nil { t.Fatal(err) } s, err := jose.Signed(sig).Claims(claims).CompactSerialize() if err != nil { t.Fatal(err) } return s, chain[0] } now := time.Now() a1 := testAuthority(t) t1, c1 := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) t2, c2 := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), IssuedAt: jose.NewNumericDate(now), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now.Add(-time.Hour) cert.NotAfter = now.Add(-time.Minute) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) t3, c3 := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", Issuer: "step-cli", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("foobar"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badProvisioner, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("foobar"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badIssuer, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "test.example.com", Issuer: "bad-issuer", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badSubject, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/renew"}, Subject: "bad-subject", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badNotBefore, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now.Add(5 * time.Minute)), Expiry: jose.NewNumericDate(now.Add(10 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badExpiry, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now.Add(-5 * time.Minute)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badIssuedAt, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), IssuedAt: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) badAudience, _ := generateX5cToken(a1, signer, jose.Claims{ Audience: []string{"https://example.com/1.0/sign"}, Subject: "test.example.com", Issuer: "step-ca-client/1.0", NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), }, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error { cert.NotBefore = now cert.NotAfter = now.Add(time.Hour) b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil}) if err != nil { return err } cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}, Value: b, }) return nil })) type args struct { ctx context.Context ott string } tests := []struct { name string authority *Authority args args want *x509.Certificate wantErr bool }{ {"ok", a1, args{ctx, t1}, c1, false}, {"ok expired cert", a1, args{ctx, t2}, c2, false}, {"ok provisioner issuer", a1, args{ctx, t3}, c3, false}, {"fail token", a1, args{ctx, "not.a.token"}, nil, true}, {"fail token reuse", a1, args{ctx, t1}, nil, true}, {"fail token signature", a1, args{ctx, badSigner}, nil, true}, {"fail token provisioner", a1, args{ctx, badProvisioner}, nil, true}, {"fail token iss", a1, args{ctx, badIssuer}, nil, true}, {"fail token sub", a1, args{ctx, badSubject}, nil, true}, {"fail token iat", a1, args{ctx, badNotBefore}, nil, true}, {"fail token iat", a1, args{ctx, badExpiry}, nil, true}, {"fail token iat", a1, args{ctx, badIssuedAt}, nil, true}, {"fail token aud", a1, args{ctx, badAudience}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.authority.AuthorizeRenewToken(tt.args.ctx, tt.args.ott) if (err != nil) != tt.wantErr { t.Errorf("Authority.AuthorizeRenewToken() error = %+v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Authority.AuthorizeRenewToken() = %v, want %v", got, tt.want) } }) } }