diff --git a/.golangci.yml b/.golangci.yml index 4defd73f..2028654a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -51,6 +51,7 @@ linters: - deadcode - staticcheck - unused + - gosimple run: skip-dirs: diff --git a/Gopkg.toml b/Gopkg.toml index 8937374b..97fb234b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -23,9 +23,6 @@ # non-go = false # go-tests = true # unused-packages = true -[[override]] - name = "gopkg.in/alecthomas/kingpin.v3-unstable" - revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306" [[constraint]] branch = "master" diff --git a/acme/api/account.go b/acme/api/account.go index 05d6a084..fb43d4f9 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -128,7 +128,6 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink, acme.URLSafeProvisionerName(prov), true, acc.GetID())) api.JSONStatus(w, acc, httpStatus) - return } // GetUpdateAccount is the api for updating an ACME account. @@ -172,7 +171,6 @@ func (h *Handler) GetUpdateAccount(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink, acme.URLSafeProvisionerName(prov), true, acc.GetID())) api.JSON(w, acc) - return } func logOrdersByAccount(w http.ResponseWriter, oids []string) { @@ -209,5 +207,4 @@ func (h *Handler) GetOrdersByAccount(w http.ResponseWriter, r *http.Request) { } api.JSON(w, orders) logOrdersByAccount(w, orders) - return } diff --git a/acme/api/handler.go b/acme/api/handler.go index 423c08ea..11cd74f2 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -113,7 +113,6 @@ func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) { } else { w.WriteHeader(http.StatusNoContent) } - return } // GetDirectory is the ACME resource for returning a directory configuration @@ -126,7 +125,6 @@ func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { } dir := h.Auth.GetDirectory(prov) api.JSON(w, dir) - return } // GetAuthz ACME api for retrieving an Authz. @@ -149,7 +147,6 @@ func (h *Handler) GetAuthz(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.AuthzLink, acme.URLSafeProvisionerName(prov), true, authz.GetID())) api.JSON(w, authz) - return } // GetChallenge ACME api for retrieving a Challenge. @@ -191,7 +188,6 @@ func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) { w.Header().Add("Link", link(getLink(acme.AuthzLink, acme.URLSafeProvisionerName(prov), true, ch.GetAuthzID()), "up")) w.Header().Set("Location", getLink(acme.ChallengeLink, acme.URLSafeProvisionerName(prov), true, ch.GetID())) api.JSON(w, ch) - return } // GetCertificate ACME api for retrieving a Certificate. @@ -210,5 +206,4 @@ func (h *Handler) GetCertificate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8") w.Write(certBytes) - return } diff --git a/acme/api/middleware.go b/acme/api/middleware.go index 3f4c99a5..af2618bf 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -42,7 +42,6 @@ func (h *Handler) addNonce(next nextHTTP) nextHTTP { w.Header().Set("Cache-Control", "no-store") logNonce(w, nonce) next(w, r) - return } } @@ -57,7 +56,6 @@ func (h *Handler) addDirLink(next nextHTTP) nextHTTP { } w.Header().Add("Link", link(h.Auth.GetLink(acme.DirectoryLink, acme.URLSafeProvisionerName(prov), true), "index")) next(w, r) - return } } @@ -87,7 +85,6 @@ func (h *Handler) verifyContentType(next nextHTTP) nextHTTP { } api.WriteError(w, acme.MalformedErr(errors.Errorf( "expected content-type to be in %s, but got %s", expected, ct))) - return } } @@ -106,7 +103,6 @@ func (h *Handler) parseJWS(next nextHTTP) nextHTTP { } ctx := context.WithValue(r.Context(), jwsContextKey, jws) next(w, r.WithContext(ctx)) - return } } @@ -202,7 +198,6 @@ func (h *Handler) validateJWS(next nextHTTP) nextHTTP { return } next(w, r) - return } } @@ -248,7 +243,6 @@ func (h *Handler) extractJWK(next nextHTTP) nextHTTP { ctx = context.WithValue(ctx, accContextKey, acc) } next(w, r.WithContext(ctx)) - return } } @@ -275,7 +269,6 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { } ctx = context.WithValue(ctx, provisionerContextKey, p) next(w, r.WithContext(ctx)) - return } } @@ -355,7 +348,6 @@ func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP { isEmptyJSON: string(payload) == "{}", }) next(w, r.WithContext(ctx)) - return } } @@ -372,6 +364,5 @@ func (h *Handler) isPostAsGet(next nextHTTP) nextHTTP { return } next(w, r) - return } } diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index 18fafd8d..f8aa322c 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -26,7 +26,6 @@ var testBody = []byte("foo") func testNext(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return } func TestHandlerAddNonce(t *testing.T) { @@ -471,7 +470,6 @@ func TestHandlerParseJWS(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, gotRaw, expRaw) w.Write(testBody) - return }, statusCode: 200, } @@ -923,7 +921,6 @@ func TestHandlerLookupJWK(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, _jwk, jwk) w.Write(testBody) - return }, statusCode: 200, } @@ -1114,7 +1111,6 @@ func TestHandlerExtractJWK(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, _jwk.KeyID, pub.KeyID) w.Write(testBody) - return }, statusCode: 200, } @@ -1139,7 +1135,6 @@ func TestHandlerExtractJWK(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, _jwk.KeyID, pub.KeyID) w.Write(testBody) - return }, statusCode: 200, } @@ -1448,7 +1443,6 @@ func TestHandlerValidateJWS(t *testing.T) { ctx: context.WithValue(context.Background(), jwsContextKey, jws), next: func(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return }, statusCode: 200, } @@ -1479,7 +1473,6 @@ func TestHandlerValidateJWS(t *testing.T) { ctx: context.WithValue(context.Background(), jwsContextKey, jws), next: func(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return }, statusCode: 200, } @@ -1510,7 +1503,6 @@ func TestHandlerValidateJWS(t *testing.T) { ctx: context.WithValue(context.Background(), jwsContextKey, jws), next: func(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return }, statusCode: 200, } diff --git a/acme/api/order.go b/acme/api/order.go index 83d1e26e..1d491102 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -97,7 +97,6 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID())) api.JSONStatus(w, o, http.StatusCreated) - return } // GetOrder ACME api for retrieving an order. @@ -121,7 +120,6 @@ func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID())) api.JSON(w, o) - return } // FinalizeOrder attemptst to finalize an order and create a certificate. @@ -160,5 +158,4 @@ func (h *Handler) FinalizeOrder(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.ID)) api.JSON(w, o) - return } diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 2d78e5a2..d1933d47 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -69,10 +69,12 @@ func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error return nil, errors.Errorf("unexpected method type %d in context", m) } return []SignOption{ - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeACME, p.Name, ""), - newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators defaultPublicKeyValidator{}, + newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), }, nil } diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index c5e6b13a..51231ba3 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -155,28 +155,23 @@ func TestACME_AuthorizeSign(t *testing.T) { if assert.NotNil(t, got) { assert.Len(t, 4, got) - _pdd := got[0] - pdd, ok := _pdd.(profileDefaultDuration) - assert.True(t, ok) - assert.Equals(t, pdd, profileDefaultDuration(86400000000000)) - - _peo := got[1] - peo, ok := _peo.(*provisionerExtensionOption) - assert.True(t, ok) - assert.Equals(t, peo.Type, 6) - assert.Equals(t, peo.Name, "test@acme-provisioner.com") - assert.Equals(t, peo.CredentialID, "") - assert.Equals(t, peo.KeyValuePairs, nil) - - _vv := got[2] - vv, ok := _vv.(*validityValidator) - assert.True(t, ok) - assert.Equals(t, vv.min, time.Duration(300000000000)) - assert.Equals(t, vv.max, time.Duration(86400000000000)) - - _dpkv := got[3] - _, ok = _dpkv.(defaultPublicKeyValidator) - assert.True(t, ok) + for _, o := range got { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeACME)) + assert.Equals(t, v.Name, tt.prov.GetName()) + assert.Equals(t, v.CredentialID, "") + assert.Len(t, 0, v.KeyValuePairs) + case profileDefaultDuration: + assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration()) + case defaultPublicKeyValidator: + case *validityValidator: + assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + } } } }) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index c6360dbd..e1b2ef9d 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -274,8 +274,8 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if p.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(payload) @@ -296,10 +296,12 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return append(so, + // modifiers / withOptions + newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators defaultPublicKeyValidator{}, commonNameValidator(payload.Claims.Subject), - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), - newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), ), nil } @@ -466,13 +468,15 @@ func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions. &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index e5e858dd..d8252799 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -266,8 +266,8 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if p.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(claims, name) @@ -284,9 +284,11 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } return append(so, - defaultPublicKeyValidator{}, - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators + defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), ), nil } @@ -323,13 +325,15 @@ func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions. &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/collection.go b/authority/provisioner/collection.go index 906c5260..0d14a65b 100644 --- a/authority/provisioner/collection.go +++ b/authority/provisioner/collection.go @@ -129,6 +129,8 @@ func (c *Collection) LoadByCertificate(cert *x509.Certificate) (Interface, bool) return c.Load("gcp/" + string(provisioner.Name)) case TypeACME: return c.Load("acme/" + string(provisioner.Name)) + case TypeX5C: + return c.Load("x5c/" + string(provisioner.Name)) default: return c.Load(string(provisioner.CredentialID)) } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 63492fd4..b2ec509a 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -213,8 +213,8 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if p.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(claims) @@ -237,9 +237,11 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return append(so, - defaultPublicKeyValidator{}, - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators + defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), ), nil } @@ -378,13 +380,15 @@ func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index cdc90ff6..f9178bb7 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -141,9 +141,9 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er return nil, err } - // Check for SSH token - if claims.Step != nil && claims.Step.SSH != nil { - if p.claimer.IsSSHCAEnabled() == false { + // Check for SSH sign-ing request. + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(claims) @@ -158,13 +158,15 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er dnsNames, ips, emails := x509util.SplitSANs(claims.SANs) return []SignOption{ - defaultPublicKeyValidator{}, + // modifiers / withOptions + newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators commonNameValidator(claims.Subject), + defaultPublicKeyValidator{}, dnsNamesValidator(dnsNames), - ipAddressesValidator(ips), emailAddressesValidator(emails), - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), - newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID), + ipAddressesValidator(ips), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), }, nil } @@ -180,6 +182,9 @@ func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error { // authorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { t := now() + if claims.Step == nil || claims.Step.SSH == nil { + return nil, errors.New("authorization token must be an SSH provisioning token") + } opts := claims.Step.SSH signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token @@ -206,13 +211,15 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) return append(signOptions, - // set the default extensions + // Set the default extensions. &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require and validate all the default fields in the SSH certificate. &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index 0e7ed57a..185f1596 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -6,33 +6,16 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" - "errors" + "net" "strings" "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/cli/jose" ) -var ( - defaultDisableRenewal = false - defaultEnableSSHCA = true - globalProvisionerClaims = Claims{ - MinTLSDur: &Duration{5 * time.Minute}, - MaxTLSDur: &Duration{24 * time.Hour}, - DefaultTLSDur: &Duration{24 * time.Hour}, - DisableRenewal: &defaultDisableRenewal, - MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs - MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, - DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, - MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs - MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - EnableSSHCA: &defaultEnableSSHCA, - } -) - func TestJWK_Getters(t *testing.T) { p, err := generateJWK() assert.FatalError(t, err) @@ -247,7 +230,7 @@ func TestJWK_AuthorizeSign(t *testing.T) { key1, err := decryptJSONWebKey(p1.EncryptedKey) assert.FatalError(t, err) - t1, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key1) + t1, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{"127.0.0.1", "max@smallstep.com", "foo"}, time.Now(), key1) assert.FatalError(t, err) t2, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{}, time.Now(), key1) @@ -260,14 +243,17 @@ func TestJWK_AuthorizeSign(t *testing.T) { token string } tests := []struct { - name string - prov *JWK - args args - err error + name string + prov *JWK + args args + err error + dns []string + emails []string + ips []net.IP }{ - {"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")}, - {"ok-sans", p1, args{t1}, nil}, - {"ok-no-sans", p1, args{t2}, nil}, + {name: "fail-signature", prov: p1, args: args{failSig}, err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")}, + {"ok-sans", p1, args{t1}, nil, []string{"foo"}, []string{"max@smallstep.com"}, []net.IP{net.ParseIP("127.0.0.1")}}, + {"ok-no-sans", p1, args{t2}, nil, []string{"subject"}, []string{}, []net.IP{}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -279,19 +265,30 @@ func TestJWK_AuthorizeSign(t *testing.T) { } else { if assert.NotNil(t, got) { assert.Len(t, 8, got) - - _cnv := got[1] - cnv, ok := _cnv.(commonNameValidator) - assert.True(t, ok) - assert.Equals(t, string(cnv), "subject") - - _dnv := got[2] - dnv, ok := _dnv.(dnsNamesValidator) - assert.True(t, ok) - if tt.name == "ok-sans" { - assert.Equals(t, []string(dnv), []string{"test.smallstep.com"}) - } else { - assert.Equals(t, []string(dnv), []string{"subject"}) + for _, o := range got { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeJWK)) + assert.Equals(t, v.Name, tt.prov.GetName()) + assert.Equals(t, v.CredentialID, tt.prov.Key.KeyID) + assert.Len(t, 0, v.KeyValuePairs) + case profileDefaultDuration: + assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration()) + case commonNameValidator: + assert.Equals(t, string(v), "subject") + case defaultPublicKeyValidator: + case dnsNamesValidator: + assert.Equals(t, []string(v), tt.dns) + case emailAddressesValidator: + assert.Equals(t, []string(v), tt.emails) + case ipAddressesValidator: + assert.Equals(t, []net.IP(v), tt.ips) + case *validityValidator: + assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } } } } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index d4937470..b65d9b6f 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -285,26 +285,19 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if o.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !o.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID()) } return o.authorizeSSHSign(claims) } - // Admins should be able to authorize any SAN - if o.IsAdmin(claims.Email) { - return []SignOption{ - profileDefaultDuration(o.claimer.DefaultTLSCertDuration()), - newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), - newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()), - }, nil - } - so := []SignOption{ - defaultPublicKeyValidator{}, - profileDefaultDuration(o.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), + profileDefaultDuration(o.claimer.DefaultTLSCertDuration()), + // validators + defaultPublicKeyValidator{}, newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()), } // Admins should be able to authorize any SAN @@ -350,13 +343,15 @@ func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{o.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(o.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{o.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index 6e99a0fb..516e0f0e 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/cli/jose" ) @@ -298,11 +299,31 @@ func TestOIDC_AuthorizeSign(t *testing.T) { if err != nil { assert.Nil(t, got) } else { - assert.NotNil(t, got) - if tt.name == "admin" { - assert.Len(t, 3, got) - } else { - assert.Len(t, 5, got) + if assert.NotNil(t, got) { + if tt.name == "admin" { + assert.Len(t, 4, got) + } else { + assert.Len(t, 5, got) + } + for _, o := range got { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeOIDC)) + assert.Equals(t, v.Name, tt.prov.GetName()) + assert.Equals(t, v.CredentialID, tt.prov.ClientID) + assert.Len(t, 0, v.KeyValuePairs) + case profileDefaultDuration: + assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration()) + case defaultPublicKeyValidator: + case *validityValidator: + assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) + case emailOnlyIdentity: + assert.Equals(t, string(v), "name@smallstep.com") + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + } } } }) diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 2a63161c..155e34de 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -86,6 +86,8 @@ const ( TypeAzure Type = 5 // TypeACME is used to indicate the ACME provisioners. TypeACME Type = 6 + // TypeX5C is used to indicate the X5C provisioners. + TypeX5C Type = 7 // RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map. RevokeAudienceKey = "revoke" @@ -108,6 +110,8 @@ func (t Type) String() string { return "Azure" case TypeACME: return "ACME" + case TypeX5C: + return "X5C" default: return "" } @@ -157,6 +161,8 @@ func (l *List) UnmarshalJSON(data []byte) error { p = &Azure{} case "acme": p = &ACME{} + case "x5c": + p = &X5C{} default: // Skip unsupported provisioners. A client using this method may be // compiled with a version of smallstep/certificates that does not diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index d535dfb6..53921a3c 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -53,19 +53,6 @@ func (v profileWithOption) Option(Options) x509util.WithOption { return x509util.WithOption(v) } -// profileDefaultDuration is a wrapper against x509util.WithOption to conform the -// interface. -type profileDefaultDuration time.Duration - -func (v profileDefaultDuration) Option(so Options) x509util.WithOption { - notBefore := so.NotBefore.Time() - if notBefore.IsZero() { - notBefore = time.Now() - } - notAfter := so.NotAfter.RelativeTime(notBefore) - return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v)) -} - // emailOnlyIdentity is a CertificateRequestValidator that checks that the only // SAN provided is the given email address. type emailOnlyIdentity string @@ -197,7 +184,61 @@ func (v emailAddressesValidator) Valid(req *x509.CertificateRequest) error { return nil } -// validityValidator validates the certificate temporal validity settings. +// profileDefaultDuration is a wrapper against x509util.WithOption to conform +// the SignOption interface. +type profileDefaultDuration time.Duration + +func (v profileDefaultDuration) Option(so Options) x509util.WithOption { + notBefore := so.NotBefore.Time() + if notBefore.IsZero() { + notBefore = time.Now() + } + notAfter := so.NotAfter.RelativeTime(notBefore) + return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v)) +} + +// profileLimitDuration is an x509 profile option that modifies an x509 validity +// period according to an imposed expiration time. +type profileLimitDuration struct { + def time.Duration + notAfter time.Time +} + +// Option returns an x509util option that limits the validity period of a +// certificate to one that is superficially imposed. +func (v profileLimitDuration) Option(so Options) x509util.WithOption { + return func(p x509util.Profile) error { + n := now() + notBefore := so.NotBefore.Time() + if notBefore.IsZero() { + notBefore = n + } + if notBefore.After(v.notAfter) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate notBefore (%s)", v.notAfter, notBefore) + } + + notAfter := so.NotAfter.RelativeTime(notBefore) + if notAfter.After(v.notAfter) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate notAfter (%s)", v.notAfter, notBefore) + } + if notAfter.IsZero() { + t := notBefore.Add(v.def) + if t.After(v.notAfter) { + notAfter = v.notAfter + } else { + notAfter = t + } + } + crt := p.Subject() + crt.NotBefore = notBefore + crt.NotAfter = notAfter + return nil + } +} + +// validityValidator validates the certificate validity settings. type validityValidator struct { min time.Duration max time.Duration @@ -208,7 +249,8 @@ func newValidityValidator(min, max time.Duration) *validityValidator { return &validityValidator{min: min, max: max} } -// Validate validates the certificate temporal validity settings. +// Valid validates the certificate validity settings (notBefore/notAfter) and +// and total duration. func (v *validityValidator) Valid(crt *x509.Certificate) error { var ( na = crt.NotAfter diff --git a/authority/provisioner/sign_options_test.go b/authority/provisioner/sign_options_test.go index 38f1d5e6..8a452dab 100644 --- a/authority/provisioner/sign_options_test.go +++ b/authority/provisioner/sign_options_test.go @@ -273,3 +273,87 @@ func Test_validityValidator_Valid(t *testing.T) { }) } } + +func Test_profileLimitDuration_Option(t *testing.T) { + n := now() + type test struct { + pld profileLimitDuration + so Options + cert *x509.Certificate + valid func(*x509.Certificate) + err error + } + tests := map[string]func() test{ + "fail/notBefore-after-limit": func() test { + d, err := ParseTimeDuration("8h") + assert.FatalError(t, err) + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: d}, + cert: new(x509.Certificate), + err: errors.New("provisioning credential expiration ("), + } + }, + "fail/requested-notAfter-after-limit": func() test { + d, err := ParseTimeDuration("4h") + assert.FatalError(t, err) + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d}, + cert: new(x509.Certificate), + err: errors.New("provisioning credential expiration ("), + } + }, + "ok/valid-notAfter-requested": func() test { + d, err := ParseTimeDuration("2h") + assert.FatalError(t, err) + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) + assert.Equals(t, cert.NotAfter, n.Add(5*time.Hour)) + }, + } + }, + "ok/valid-notAfter-nil-limit-over-default": func() test { + return test{ + pld: profileLimitDuration{def: 1 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) + assert.Equals(t, cert.NotAfter, n.Add(4*time.Hour)) + }, + } + }, + "ok/valid-notAfter-nil-limit-under-default": func() test { + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) + assert.Equals(t, cert.NotAfter, n.Add(6*time.Hour)) + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tt := run() + prof := &x509util.Leaf{} + prof.SetSubject(tt.cert) + if err := tt.pld.Option(tt.so)(prof); err != nil { + if assert.NotNil(t, tt.err) { + assert.HasPrefix(t, err.Error(), tt.err.Error()) + } + } else { + if assert.Nil(t, tt.err) { + tt.valid(prof.Subject()) + } + } + }) + } +} diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 94a77c50..a8f63cd5 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -191,47 +191,67 @@ func (m *sshDefaultExtensionModifier) Modify(cert *ssh.Certificate) error { } } -// sshCertificateValidityModifier is a SSHCertificateModifier checks the +// sshValidityModifier is an SSHCertificateModifier that checks the // validity bounds, setting them if they are not provided. It will fail if a // CertType has not been set or is not valid. -type sshCertificateValidityModifier struct { +type sshValidityModifier struct { *Claimer + validBefore time.Time } -func (m *sshCertificateValidityModifier) Modify(cert *ssh.Certificate) error { - var d, min, max time.Duration +func (m *sshValidityModifier) Modify(cert *ssh.Certificate) error { + var d time.Duration + switch cert.CertType { case ssh.UserCert: d = m.DefaultUserSSHCertDuration() - min = m.MinUserSSHCertDuration() - max = m.MaxUserSSHCertDuration() case ssh.HostCert: d = m.DefaultHostSSHCertDuration() - min = m.MinHostSSHCertDuration() - max = m.MaxHostSSHCertDuration() case 0: return errors.New("ssh certificate type has not been set") default: return errors.Errorf("unknown ssh certificate type %d", cert.CertType) } + hasLimit := !m.validBefore.IsZero() + + n := now() if cert.ValidAfter == 0 { - cert.ValidAfter = uint64(now().Truncate(time.Second).Unix()) + cert.ValidAfter = uint64(n.Truncate(time.Second).Unix()) } - if cert.ValidBefore == 0 { - t := time.Unix(int64(cert.ValidAfter), 0) - cert.ValidBefore = uint64(t.Add(d).Unix()) + certValidAfter := time.Unix(int64(cert.ValidAfter), 0) + if hasLimit && certValidAfter.After(m.validBefore) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate validAfter (%s)", m.validBefore, certValidAfter) } - diff := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second - switch { - case diff < min: - return errors.Errorf("ssh certificate duration cannot be lower than %s", min) - case diff > max: - return errors.Errorf("ssh certificate duration cannot be greater than %s", max) - default: - return nil + if cert.ValidBefore == 0 { + certValidBefore := certValidAfter.Add(d) + if hasLimit && m.validBefore.Before(certValidBefore) { + certValidBefore = m.validBefore + } + cert.ValidBefore = uint64(certValidBefore.Unix()) + } else if hasLimit { + certValidBefore := time.Unix(int64(cert.ValidBefore), 0) + if m.validBefore.Before(certValidBefore) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate validBefore (%s)", m.validBefore, certValidBefore) + } } + + return nil +} + +func sshDefaultValidityModifier(c *Claimer) SSHCertificateModifier { + return &sshValidityModifier{c, time.Time{}} +} + +// sshLimitValidityModifier adjusts the duration to +// min(default, remaining provisioning credential duration). +// E.g. if the default is 12hrs but the remaining validity of the provisioning +// credential is only 4hrs, this option will set the value to 4hrs (the min of the two values). +func sshLimitValidityModifier(c *Claimer, validBefore time.Time) SSHCertificateModifier { + return &sshValidityModifier{c, validBefore} } // sshCertificateOptionsValidator validates the user SSHOptions with the ones @@ -245,6 +265,48 @@ func (v sshCertificateOptionsValidator) Valid(got SSHOptions) error { return want.match(got) } +type sshCertificateValidityValidator struct { + *Claimer +} + +func (v *sshCertificateValidityValidator) Valid(cert *ssh.Certificate) error { + switch { + case cert.ValidAfter == 0: + return errors.New("ssh certificate validAfter cannot be 0") + case cert.ValidBefore < uint64(now().Unix()): + return errors.New("ssh certificate validBefore cannot be in the past") + case cert.ValidBefore < cert.ValidAfter: + return errors.New("ssh certificate validBefore cannot be before validAfter") + } + + var min, max time.Duration + switch cert.CertType { + case ssh.UserCert: + min = v.MinUserSSHCertDuration() + max = v.MaxUserSSHCertDuration() + case ssh.HostCert: + min = v.MinHostSSHCertDuration() + max = v.MaxHostSSHCertDuration() + case 0: + return errors.New("ssh certificate type has not been set") + default: + return errors.Errorf("unknown ssh certificate type %d", cert.CertType) + } + + // seconds + dur := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second + switch { + case dur < min: + return errors.Errorf("requested duration of %s is less than minimum "+ + "accepted duration for selected provisioner of %s", dur, min) + case dur > max: + return errors.Errorf("requested duration of %s is greater than maximum "+ + "accepted duration for selected provisioner of %s", dur, max) + default: + return nil + } +} + // sshCertificateDefaultValidator implements a simple validator for all the // fields in the SSH certificate. type sshCertificateDefaultValidator struct{} diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index 95c6913c..25a44121 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -190,3 +190,197 @@ func Test_sshCertificateDefaultValidator_Valid(t *testing.T) { }) } } + +func Test_sshCertificateValidityValidator(t *testing.T) { + p, err := generateX5C(nil) + assert.FatalError(t, err) + v := sshCertificateValidityValidator{p.claimer} + n := now() + tests := []struct { + name string + cert *ssh.Certificate + err error + }{ + { + "fail/validAfter-0", + &ssh.Certificate{CertType: ssh.UserCert}, + errors.New("ssh certificate validAfter cannot be 0"), + }, + { + "fail/validBefore-in-past", + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(-time.Minute).Unix())}, + errors.New("ssh certificate validBefore cannot be in the past"), + }, + { + "fail/validBefore-before-validAfter", + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Add(5 * time.Minute).Unix()), ValidBefore: uint64(now().Add(3 * time.Minute).Unix())}, + errors.New("ssh certificate validBefore cannot be before validAfter"), + }, + { + "fail/cert-type-not-set", + &ssh.Certificate{ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(10 * time.Minute).Unix())}, + errors.New("ssh certificate type has not been set"), + }, + { + "fail/unexpected-cert-type", + &ssh.Certificate{ + CertType: 3, + ValidAfter: uint64(now().Unix()), + ValidBefore: uint64(now().Add(10 * time.Minute).Unix()), + }, + errors.New("unknown ssh certificate type 3"), + }, + { + "fail/durationmax", + &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(48 * time.Hour).Unix()), + }, + errors.New("requested duration of 48h0m0s is greater than maximum accepted duration for selected provisioner of 24h0m0s"), + }, + { + "ok", + &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(now().Unix()), + ValidBefore: uint64(now().Add(8 * time.Hour).Unix()), + }, + nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := v.Valid(tt.cert); err != nil { + if assert.NotNil(t, tt.err) { + assert.HasPrefix(t, err.Error(), tt.err.Error()) + } + } else { + assert.Nil(t, tt.err) + } + }) + } +} + +func Test_sshValidityModifier(t *testing.T) { + n := now() + p, err := generateX5C(nil) + assert.FatalError(t, err) + type test struct { + svm *sshValidityModifier + cert *ssh.Certificate + valid func(*ssh.Certificate) + err error + } + tests := map[string]func() test{ + "fail/type-not-set": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)}, + cert: &ssh.Certificate{ + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), + }, + err: errors.New("ssh certificate type has not been set"), + } + }, + "fail/type-not-recognized": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 4, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), + }, + err: errors.New("unknown ssh certificate type 4"), + } + }, + "fail/requested-validAfter-after-limit": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Add(2 * time.Hour).Unix()), + ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), + }, + err: errors.Errorf("provisioning credential expiration ("), + } + }, + "fail/requested-validBefore-after-limit": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(2 * time.Hour).Unix()), + }, + err: errors.New("provisioning credential expiration ("), + } + }, + "ok/valid-requested-validBefore": func() test { + va, vb := uint64(n.Unix()), uint64(n.Add(2*time.Hour).Unix()) + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: va, + ValidBefore: vb, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, vb) + }, + } + }, + "ok/empty-requested-validBefore-limit-after-default": func() test { + va := uint64(n.Unix()) + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(5 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: va, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, uint64(n.Add(4*time.Hour).Unix())) + }, + } + }, + "ok/empty-requested-validBefore-limit-before-default": func() test { + va := uint64(n.Unix()) + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: va, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, uint64(n.Add(3*time.Hour).Unix())) + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tt := run() + if err := tt.svm.Modify(tt.cert); err != nil { + if assert.NotNil(t, tt.err) { + assert.HasPrefix(t, err.Error(), tt.err.Error()) + } + } else { + if assert.Nil(t, tt.err) { + tt.valid(tt.cert) + } + } + }) + } +} diff --git a/authority/provisioner/testdata/x5c-leaf.crt b/authority/provisioner/testdata/x5c-leaf.crt new file mode 100644 index 00000000..2d674c11 --- /dev/null +++ b/authority/provisioner/testdata/x5c-leaf.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5 +MDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr +VpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC +BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW +f4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp +czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN +wtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z +AvUgkUQ2G25NBRmX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE----- diff --git a/authority/provisioner/testdata/x5c-leaf.key b/authority/provisioner/testdata/x5c-leaf.key new file mode 100644 index 00000000..f77d015e --- /dev/null +++ b/authority/provisioner/testdata/x5c-leaf.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIALytC4LyTTAagMLMv+rzq2vtfhFkhuyBz4kqsnRs6zioAoGCCqGSM49 +AwEHoUQDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvrVpgS +PXYruNRFduPWX564Abz/TDmb276JbKGeQg== +-----END EC PRIVATE KEY----- diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 2760a16b..8f4fbaad 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -19,9 +19,44 @@ import ( "github.com/smallstep/cli/jose" ) -var testAudiences = Audiences{ - Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"}, - Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"}, +var ( + defaultDisableRenewal = false + defaultEnableSSHCA = true + globalProvisionerClaims = Claims{ + MinTLSDur: &Duration{5 * time.Minute}, + MaxTLSDur: &Duration{24 * time.Hour}, + DefaultTLSDur: &Duration{24 * time.Hour}, + DisableRenewal: &defaultDisableRenewal, + MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs + MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, + DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, + MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs + MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + EnableSSHCA: &defaultEnableSSHCA, + } + testAudiences = Audiences{ + Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"}, + Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"}, + } +) + +func provisionerClaims() *Claims { + ddr := false + des := true + return &Claims{ + MinTLSDur: &Duration{5 * time.Minute}, + MaxTLSDur: &Duration{24 * time.Hour}, + DefaultTLSDur: &Duration{24 * time.Hour}, + DisableRenewal: &ddr, + MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs + MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, + DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, + MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs + MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + EnableSSHCA: &des, + } } const awsTestCertificate = `-----BEGIN CERTIFICATE----- @@ -162,6 +197,58 @@ func generateJWK() (*JWK, error) { }, nil } +func generateX5C(root []byte) (*X5C, error) { + if root == nil { + root = []byte(`-----BEGIN CERTIFICATE----- +MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES +MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz +OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO +BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T +AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD +VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH +1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY +M46l92gdOozT +-----END CERTIFICATE-----`) + } + + name, err := randutil.Alphanumeric(10) + if err != nil { + return nil, err + } + claimer, err := NewClaimer(nil, globalProvisionerClaims) + if err != nil { + return nil, err + } + + rootPool := x509.NewCertPool() + + var ( + block *pem.Block + rest = root + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block") + } + rootPool.AddCert(cert) + } + return &X5C{ + Name: name, + Type: "X5C", + Roots: root, + Claims: &globalProvisionerClaims, + audiences: testAudiences, + claimer: claimer, + rootPool: rootPool, + }, nil +} + func generateOIDC() (*OIDC, error) { name, err := randutil.Alphanumeric(10) if err != nil { @@ -446,11 +533,31 @@ func generateSimpleToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) return generateToken("subject", iss, aud, "name@smallstep.com", []string{"test.smallstep.com"}, time.Now(), jwk) } -func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, 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), - ) +type tokOption func(*jose.SignerOptions) error + +func withX5CHdr(certs []*x509.Certificate) tokOption { + return func(so *jose.SignerOptions) error { + strs := make([]string, len(certs)) + for i, cert := range certs { + strs[i] = base64.StdEncoding.EncodeToString(cert.Raw) + } + so.WithHeader("x5c", strs) + return nil + } +} + +func generateToken(sub, iss, aud string, email 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 } @@ -742,3 +849,24 @@ func generateACME() (*ACME, error) { } return p, nil } + +func parseCerts(b []byte) ([]*x509.Certificate, error) { + var ( + block *pem.Block + rest = b + certs = []*x509.Certificate{} + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block") + } + + certs = append(certs, cert) + } + return certs, nil +} diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go new file mode 100644 index 00000000..55725982 --- /dev/null +++ b/authority/provisioner/x5c.go @@ -0,0 +1,267 @@ +package provisioner + +import ( + "context" + "crypto/x509" + "encoding/pem" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/x509util" + "github.com/smallstep/cli/jose" +) + +// x5cPayload extends jwt.Claims with step attributes. +type x5cPayload struct { + jose.Claims + SANs []string `json:"sans,omitempty"` + Step *stepPayload `json:"step,omitempty"` + chains [][]*x509.Certificate +} + +// X5C is the default provisioner, an entity that can sign tokens necessary for +// signature requests. +type X5C struct { + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + claimer *Claimer + audiences Audiences + rootPool *x509.CertPool +} + +// GetID returns the provisioner unique identifier. The name and credential id +// should uniquely identify any X5C provisioner. +func (p *X5C) GetID() string { + return "x5c/" + p.Name +} + +// GetTokenID returns the identifier of the token. +func (p *X5C) GetTokenID(ott string) (string, error) { + // Validate payload + token, err := jose.ParseSigned(ott) + if err != nil { + return "", errors.Wrap(err, "error parsing token") + } + + // Get claims w/out verification. We need to look up the provisioner + // key in order to verify the claims and we need the issuer from the claims + // before we can look up the provisioner. + var claims jose.Claims + if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { + return "", errors.Wrap(err, "error verifying claims") + } + return claims.ID, nil +} + +// GetName returns the name of the provisioner. +func (p *X5C) GetName() string { + return p.Name +} + +// GetType returns the type of provisioner. +func (p *X5C) GetType() Type { + return TypeX5C +} + +// GetEncryptedKey returns the base provisioner encrypted key if it's defined. +func (p *X5C) GetEncryptedKey() (string, string, bool) { + return "", "", false +} + +// Init initializes and validates the fields of a X5C type. +func (p *X5C) Init(config Config) error { + switch { + case p.Type == "": + return errors.New("provisioner type cannot be empty") + case p.Name == "": + return errors.New("provisioner name cannot be empty") + case len(p.Roots) == 0: + return errors.New("provisioner root(s) cannot be empty") + } + + p.rootPool = x509.NewCertPool() + + var ( + block *pem.Block + rest = p.Roots + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.Wrap(err, "error parsing x509 certificate from PEM block") + } + p.rootPool.AddCert(cert) + } + + // Verify that at least one root was found. + if len(p.rootPool.Subjects()) == 0 { + return errors.Errorf("no x509 certificates found in roots attribute for provisioner %s", p.GetName()) + } + + // Update claims with global ones + var err error + if p.claimer, err = NewClaimer(p.Claims, config.Claims); err != nil { + return err + } + + p.audiences = config.Audiences.WithFragment(p.GetID()) + return nil +} + +// authorizeToken performs common jwt authorization actions and returns the +// claims for case specific downstream parsing. +// e.g. a Sign request will auth/validate different fields than a Revoke request. +func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, error) { + jwt, err := jose.ParseSigned(token) + if err != nil { + return nil, errors.Wrapf(err, "error parsing token") + } + + verifiedChains, err := jwt.Headers[0].Certificates(x509.VerifyOptions{ + Roots: p.rootPool, + }) + if err != nil { + return nil, errors.Wrap(err, "error verifying x5c certificate chain") + } + leaf := verifiedChains[0][0] + + if leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 { + return nil, errors.New("certificate used to sign x5c token cannot be used for digital signature") + } + + // Using the leaf certificates key to validate the claims accomplishes two + // things: + // 1. Asserts that the private key used to sign the token corresponds + // to the public certificate in the `x5c` header of the token. + // 2. Asserts that the claims are valid - have not been tampered with. + var claims x5cPayload + if err = jwt.Claims(leaf.PublicKey, &claims); err != nil { + return nil, errors.Wrap(err, "error parsing claims") + } + + // According to "rfc7519 JSON Web Token" acceptable skew should be no + // more than a few minutes. + if err = claims.ValidateWithLeeway(jose.Expected{ + Issuer: p.Name, + Time: time.Now().UTC(), + }, time.Minute); err != nil { + return nil, errors.Wrapf(err, "invalid token") + } + + // validate audiences with the defaults + if !matchesAudience(claims.Audience, audiences) { + return nil, errors.New("invalid token: invalid audience claim (aud)") + } + + if claims.Subject == "" { + return nil, errors.New("token subject cannot be empty") + } + + // Save the verified chains on the x5c payload object. + claims.chains = verifiedChains + return &claims, nil +} + +// AuthorizeRevoke returns an error if the provisioner does not have rights to +// revoke the certificate with serial number in the `sub` property. +func (p *X5C) AuthorizeRevoke(token string) error { + _, err := p.authorizeToken(token, p.audiences.Revoke) + return err +} + +// AuthorizeSign validates the given token. +func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { + claims, err := p.authorizeToken(token, p.audiences.Sign) + if err != nil { + return nil, err + } + + // Check for SSH sign-ing request. + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } + return p.authorizeSSHSign(claims) + } + + // NOTE: This is for backwards compatibility with older versions of cli + // and certificates. Older versions added the token subject as the only SAN + // in a CSR by default. + if len(claims.SANs) == 0 { + claims.SANs = []string{claims.Subject} + } + + dnsNames, ips, emails := x509util.SplitSANs(claims.SANs) + + return []SignOption{ + // modifiers / withOptions + newProvisionerExtensionOption(TypeX5C, p.Name, ""), + profileLimitDuration{p.claimer.DefaultTLSCertDuration(), claims.chains[0][0].NotAfter}, + // validators + commonNameValidator(claims.Subject), + defaultPublicKeyValidator{}, + dnsNamesValidator(dnsNames), + emailAddressesValidator(emails), + ipAddressesValidator(ips), + newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + }, nil +} + +// AuthorizeRenewal returns an error if the renewal is disabled. +func (p *X5C) AuthorizeRenewal(cert *x509.Certificate) error { + if p.claimer.IsDisableRenewal() { + return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + } + return nil +} + +// authorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *X5C) authorizeSSHSign(claims *x5cPayload) ([]SignOption, error) { + if claims.Step == nil || claims.Step.SSH == nil { + return nil, errors.New("authorization token must be an SSH provisioning token") + } + opts := claims.Step.SSH + signOptions := []SignOption{ + // validates user's SSHOptions with the ones in the token + sshCertificateOptionsValidator(*opts), + // set the key id to the token subject + sshCertificateKeyIDModifier(claims.Subject), + } + + // Add modifiers from custom claims + if opts.CertType != "" { + signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) + } + if len(opts.Principals) > 0 { + signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals)) + } + t := now() + if !opts.ValidAfter.IsZero() { + signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) + } + if !opts.ValidBefore.IsZero() { + signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) + } + + // Default to a user certificate with no principals if not set + signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) + + return append(signOptions, + // Set the default extensions. + &sshDefaultExtensionModifier{}, + // Checks the validity bounds, and set the validity if has not been set. + sshLimitValidityModifier(p.claimer, claims.chains[0][0].NotAfter), + // Validate public key. + &sshDefaultPublicKeyValidator{}, + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate + &sshCertificateDefaultValidator{}, + ), nil +} diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go new file mode 100644 index 00000000..477e3267 --- /dev/null +++ b/authority/provisioner/x5c_test.go @@ -0,0 +1,751 @@ +package provisioner + +import ( + "context" + "crypto/x509" + "net" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/jose" +) + +func TestX5C_Getters(t *testing.T) { + p, err := generateX5C(nil) + assert.FatalError(t, err) + id := "x5c/" + p.Name + if got := p.GetID(); got != id { + t.Errorf("X5C.GetID() = %v, want %v:%v", got, p.Name, id) + } + if got := p.GetName(); got != p.Name { + t.Errorf("X5C.GetName() = %v, want %v", got, p.Name) + } + if got := p.GetType(); got != TypeX5C { + t.Errorf("X5C.GetType() = %v, want %v", got, TypeX5C) + } + kid, key, ok := p.GetEncryptedKey() + if kid != "" || key != "" || ok == true { + t.Errorf("X5C.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)", + kid, key, ok, "", "", false) + } +} + +func TestX5C_Init(t *testing.T) { + type ProvisionerValidateTest struct { + p *X5C + err error + extraValid func(*X5C) error + } + tests := map[string]func(*testing.T) ProvisionerValidateTest{ + "fail/empty": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{}, + err: errors.New("provisioner type cannot be empty"), + } + }, + "fail/empty-name": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{ + Type: "X5C", + }, + err: errors.New("provisioner name cannot be empty"), + } + }, + "fail/empty-type": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{Name: "foo"}, + err: errors.New("provisioner type cannot be empty"), + } + }, + "fail/empty-key": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{Name: "foo", Type: "bar"}, + err: errors.New("provisioner root(s) cannot be empty"), + } + }, + "fail/no-valid-root-certs": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{Name: "foo", Type: "bar", Roots: []byte("foo"), audiences: testAudiences}, + err: errors.Errorf("no x509 certificates found in roots attribute for provisioner foo"), + } + }, + "fail/invalid-duration": func(t *testing.T) ProvisionerValidateTest { + p, err := generateX5C(nil) + assert.FatalError(t, err) + p.Claims = &Claims{DefaultTLSDur: &Duration{0}} + return ProvisionerValidateTest{ + p: p, + err: errors.New("claims: DefaultTLSCertDuration must be greater than 0"), + } + }, + "ok": func(t *testing.T) ProvisionerValidateTest { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return ProvisionerValidateTest{ + p: p, + } + }, + "ok/root-chain": func(t *testing.T) ProvisionerValidateTest { + p, err := generateX5C([]byte(`-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES +MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz +OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO +BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T +AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD +VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH +1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY +M46l92gdOozT +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + return ProvisionerValidateTest{ + p: p, + extraValid: func(p *X5C) error { + numCerts := len(p.rootPool.Subjects()) + if numCerts != 2 { + return errors.Errorf("unexpected number of certs: want 2, but got %d", numCerts) + } + return nil + }, + } + }, + } + + config := Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + } + for name, get := range tests { + t.Run(name, func(t *testing.T) { + tc := get(t) + err := tc.p.Init(config) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.Equals(t, tc.err.Error(), err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, tc.p.audiences, config.Audiences.WithFragment(tc.p.GetID())) + if tc.extraValid != nil { + assert.Nil(t, tc.extraValid(tc.p)) + } + } + } + }) + } +} + +func TestX5C_authorizeToken(t *testing.T) { + type test struct { + p *X5C + token string + err error + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + err: errors.New("error parsing token"), + } + }, + "fail/invalid-cert-chain": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBpTCCAUugAwIBAgIRAOn2LHXjYyTXQ7PNjDTSKiIwCgYIKoZIzj0EAwIwHDEa +MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwOTE0MDk1NTM2WhcNMjkw +OTExMDk1NTM2WjAkMSIwIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENB +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2Cs0TY0dLM4b2s+z8+cc3JJp/W5H +zQRvICX/1aJ4MuObNLcvoSguJwJEkYpGB5fhb0KvoL+ebHfEOywGNwrWkaNmMGQw +DgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNLJ +4ZXoX9cI6YkGPxgs2US3ssVzMB8GA1UdIwQYMBaAFGIwpqz85wL29aF47Vj9XSVM +P9K7MAoGCCqGSM49BAMCA0gAMEUCIQC5c1ldDcesDb31GlO5cEJvOcRrIrNtkk8m +a5wpg+9s6QIgHIW6L60F8klQX+EO3o0SBqLeNcaskA4oSZsKjEdpSGo= +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.Name, testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"), + } + }, + "fail/doubled-up-self-signed-cert": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w +DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow +EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI +3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG +M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ +BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS +ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G +EXAHTA9L +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w +DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow +EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI +3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG +M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ +BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS +ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G +EXAHTA9L +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.Name, testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"), + } + }, + "fail/digital-signature-ext-required": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBuTCCAV+gAwIBAgIQeRJLdDMIdn/T2ORKxYABezAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMjQxMTRaGA8yMTE5 +MDkwODAyNDExMlowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEDA1nGTOujobkcBWklyvymhWE5gQlvNLarVzhhhvPDw+MK2LX +yqkXrYZM10GrwQZuQ7ykHnjz00U/KXpPRQ7+0qOBiDCBhTAOBgNVHQ8BAf8EBAMC +BSAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQYv0AK +3GUOvC+m8ZTfyhn7tKQOazAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp +czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIhAPmertx0 +lchRU3kAu647exvlhEr1xosPOu6P8kVYbtTEAiAA51w9EYIT/Zb26M3eQV817T2g +Dnhl0ElPQsA92pkqbA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + + tok, err := generateToken("", p.Name, testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("certificate used to sign x5c token cannot be used for digital signature"), + } + }, + "fail/signature-does-not-match-x5c-pub-key": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5 +MDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr +VpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC +BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW +f4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp +czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN +wtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z +AvUgkUQ2G25NBRmX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", "foobar", testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive"), + } + }, + "fail/invalid-issuer": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", "foobar", testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"), + } + }, + "fail/invalid-audience": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.GetName(), "foobar", "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("invalid token: invalid audience claim (aud)"), + } + }, + "fail/empty-subject": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("token subject cannot be empty"), + } + }, + "ok": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.NotNil(t, claims) + assert.NotNil(t, claims.chains) + } + } + }) + } +} + +func TestX5C_AuthorizeSign(t *testing.T) { + type test struct { + p *X5C + token string + ctx context.Context + err error + dns []string + emails []string + ips []net.IP + } + tests := map[string]func(*testing.T) test{ + "fail/invalid-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + ctx: NewContextWithMethod(context.Background(), SignMethod), + err: errors.New("error parsing token"), + } + }, + "fail/ssh/disabled": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + p.claimer.claims = provisionerClaims() + *p.claimer.claims.EnableSSHCA = false + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignSSHMethod), + token: tok, + err: errors.Errorf("ssh ca is disabled for provisioner x5c/%s", p.GetName()), + } + }, + "fail/ssh/invalid-token": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignSSHMethod), + token: tok, + err: errors.New("authorization token must be an SSH provisioning token"), + } + }, + "ok/empty-sans": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignMethod), + token: tok, + dns: []string{"foo"}, + emails: []string{}, + ips: []net.IP{}, + } + }, + "ok/multi-sans": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"127.0.0.1", "foo", "max@smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignMethod), + token: tok, + dns: []string{"foo"}, + emails: []string{"max@smallstep.com"}, + ips: []net.IP{net.ParseIP("127.0.0.1")}, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + if assert.NotNil(t, opts) { + tot := 0 + for _, o := range opts { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeX5C)) + assert.Equals(t, v.Name, tc.p.GetName()) + assert.Equals(t, v.CredentialID, "") + assert.Len(t, 0, v.KeyValuePairs) + case profileLimitDuration: + assert.Equals(t, v.def, tc.p.claimer.DefaultTLSCertDuration()) + + claims, err := tc.p.authorizeToken(tc.token, tc.p.audiences.Sign) + assert.FatalError(t, err) + assert.Equals(t, v.notAfter, claims.chains[0][0].NotAfter) + case commonNameValidator: + assert.Equals(t, string(v), "foo") + case defaultPublicKeyValidator: + case dnsNamesValidator: + assert.Equals(t, []string(v), tc.dns) + case emailAddressesValidator: + assert.Equals(t, []string(v), tc.emails) + case ipAddressesValidator: + assert.Equals(t, []net.IP(v), tc.ips) + case *validityValidator: + assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration()) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + tot++ + } + assert.Equals(t, tot, 8) + } + } + } + }) + } +} + +func TestX5C_authorizeSSHSign(t *testing.T) { + _, fn := mockNow() + defer fn() + type test struct { + p *X5C + claims *x5cPayload + err error + } + tests := map[string]func(*testing.T) test{ + "fail/no-Step-claim": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + claims: new(x5cPayload), + err: errors.New("authorization token must be an SSH provisioning token"), + } + }, + "fail/no-SSH-subattribute-in-claims": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + claims: &x5cPayload{Step: new(stepPayload)}, + err: errors.New("authorization token must be an SSH provisioning token"), + } + }, + "ok/with-claims": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + return test{ + p: p, + claims: &x5cPayload{ + Step: &stepPayload{SSH: &SSHOptions{ + CertType: SSHHostCert, + Principals: []string{"max", "mariano", "alan"}, + ValidAfter: TimeDuration{d: 5 * time.Minute}, + ValidBefore: TimeDuration{d: 10 * time.Minute}, + }}, + Claims: jose.Claims{Subject: "foo"}, + chains: [][]*x509.Certificate{certs}, + }, + } + }, + "ok/without-claims": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + return test{ + p: p, + claims: &x5cPayload{ + Step: &stepPayload{SSH: &SSHOptions{}}, + Claims: jose.Claims{Subject: "foo"}, + chains: [][]*x509.Certificate{certs}, + }, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if opts, err := tc.p.authorizeSSHSign(tc.claims); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + if assert.NotNil(t, opts) { + tot := 0 + nw := now() + for _, o := range opts { + switch v := o.(type) { + case sshCertificateOptionsValidator: + tc.claims.Step.SSH.ValidAfter.t = time.Time{} + tc.claims.Step.SSH.ValidBefore.t = time.Time{} + assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH) + case sshCertificateKeyIDModifier: + assert.Equals(t, string(v), "foo") + case sshCertificateCertTypeModifier: + assert.Equals(t, string(v), tc.claims.Step.SSH.CertType) + case sshCertificatePrincipalsModifier: + assert.Equals(t, []string(v), tc.claims.Step.SSH.Principals) + case sshCertificateValidAfterModifier: + assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix()) + case sshCertificateValidBeforeModifier: + assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix()) + case sshCertificateDefaultsModifier: + assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert}) + case *sshValidityModifier: + assert.Equals(t, v.Claimer, tc.p.claimer) + assert.Equals(t, v.validBefore, tc.claims.chains[0][0].NotAfter) + case *sshCertificateValidityValidator: + assert.Equals(t, v.Claimer, tc.p.claimer) + case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator, + *sshCertificateDefaultValidator: + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + tot++ + } + if len(tc.claims.Step.SSH.CertType) > 0 { + assert.Equals(t, tot, 12) + } else { + assert.Equals(t, tot, 8) + } + } + } + } + }) + } +} + +func TestX5C_AuthorizeRevoke(t *testing.T) { + type test struct { + p *X5C + token string + err error + } + tests := map[string]func(*testing.T) test{ + "fail/invalid-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + err: errors.New("error parsing token"), + } + }, + "ok": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Revoke[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeRevoke(tc.token); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestX5C_AuthorizeRenewal(t *testing.T) { + p1, err := generateX5C(nil) + assert.FatalError(t, err) + p2, err := generateX5C(nil) + assert.FatalError(t, err) + + // disable renewal + disable := true + p2.Claims = &Claims{DisableRenewal: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + prov *X5C + args args + wantErr bool + }{ + {"ok", p1, args{nil}, false}, + {"fail", p2, args{nil}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("X5C.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/authority/tls_test.go b/authority/tls_test.go index 8d443fd4..f719d615 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -208,14 +208,15 @@ func TestSign(t *testing.T) { }, "fail rsa key too short": func(t *testing.T) *signTest { shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST----- -MIIBdDCB2wIBADAOMQwwCgYDVQQDEwNmb28wgaIwDQYJKoZIhvcNAQEBBQADgZAA -MIGMAoGEAK8dks7oV6kcIFEaWna7CDGYPAE8IL7rNi+ruQ1dIYz+JtxT7OPjbCn/ -t5iqni96+35iS/8CvMtEuquOMTMSWOWwlurrbTbLqCazuz/g233o8udxSxhny3cY -wHogp4cXCX6cFll6DeUnoCEuTTSIu8IBHbK48VfNw4V4gGz6cp/H93HrAgMBAAGg -ITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQHMAWCA2ZvbzANBgkqhkiG9w0BAQsF -AAOBhABCZsYM+Kgje68Z9Fjl2+cBwtQHvZDarh+cz6W1SchinZ1T0aNQvSj/otOe -ttnEF4Rq8zqzr4fbv+AF451Mx36AkfgZr9XWGzxidrH+fBCNWXWNR+ymhrL6UFTG -2FbarLt9jN2aJLAYQPwtSeGTAZ74tLOPRPnTP6aMfFNg4XCR0uveHA== +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)) assert.FatalError(t, err) diff --git a/ca/renew.go b/ca/renew.go index 44234781..6a4fd22b 100644 --- a/ca/renew.go +++ b/ca/renew.go @@ -178,7 +178,7 @@ func (r *TLSRenewer) renewCertificate() { } func (r *TLSRenewer) nextRenewDuration(notAfter time.Time) time.Duration { - d := notAfter.Sub(time.Now()) - r.renewBefore + d := time.Until(notAfter) - r.renewBefore n := rand.Int63n(int64(r.renewJitter)) d -= time.Duration(n) if d < 0 { diff --git a/ca/signal.go b/ca/signal.go index 0d950435..598cc6f6 100644 --- a/ca/signal.go +++ b/ca/signal.go @@ -28,20 +28,17 @@ func StopHandler(servers ...Stopper) { signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) defer signal.Stop(signals) - for { - select { - case sig := <-signals: - switch sig { - case syscall.SIGINT, syscall.SIGTERM: - log.Println("shutting down ...") - for _, server := range servers { - err := server.Stop() - if err != nil { - log.Printf("error stopping server: %s", err.Error()) - } + for sig := range signals { + switch sig { + case syscall.SIGINT, syscall.SIGTERM: + log.Println("shutting down ...") + for _, server := range servers { + err := server.Stop() + if err != nil { + log.Printf("error stopping server: %s", err.Error()) } - return } + return } } } @@ -54,28 +51,25 @@ func StopReloaderHandler(servers ...StopReloader) { signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) defer signal.Stop(signals) - for { - select { - case sig := <-signals: - switch sig { - case syscall.SIGHUP: - log.Println("reloading ...") - for _, server := range servers { - err := server.Reload() - if err != nil { - log.Printf("error reloading server: %+v", err) - } + for sig := range signals { + switch sig { + case syscall.SIGHUP: + log.Println("reloading ...") + for _, server := range servers { + err := server.Reload() + if err != nil { + log.Printf("error reloading server: %+v", err) } - case syscall.SIGINT, syscall.SIGTERM: - log.Println("shutting down ...") - for _, server := range servers { - err := server.Stop() - if err != nil { - log.Printf("error stopping server: %s", err.Error()) - } + } + case syscall.SIGINT, syscall.SIGTERM: + log.Println("shutting down ...") + for _, server := range servers { + err := server.Stop() + if err != nil { + log.Printf("error stopping server: %s", err.Error()) } - return } + return } } } diff --git a/ca/tls_options_test.go b/ca/tls_options_test.go index a422799e..e2ed4234 100644 --- a/ca/tls_options_test.go +++ b/ca/tls_options_test.go @@ -553,7 +553,7 @@ func equalPools(a, b *x509.CertPool) bool { for i := range subjects { sB[i] = string(subjects[i]) } - sort.Sort(sort.StringSlice(sA)) - sort.Sort(sort.StringSlice(sB)) + sort.Strings(sA) + sort.Strings(sB) return reflect.DeepEqual(sA, sB) } diff --git a/logging/handler.go b/logging/handler.go index 7a8ae0bb..c59736d9 100644 --- a/logging/handler.go +++ b/logging/handler.go @@ -32,7 +32,7 @@ func (l *LoggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := time.Now() rw := NewResponseLogger(w) l.next.ServeHTTP(rw, r) - d := time.Now().Sub(t) + d := time.Since(t) l.writeEntry(rw, r, t, d) }