diff --git a/api/api.go b/api/api.go index 438f97bf..a91c9ab8 100644 --- a/api/api.go +++ b/api/api.go @@ -11,43 +11,19 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" - "github.com/smallstep/ca-component/provisioner" + "github.com/smallstep/ca-component/authority" "github.com/smallstep/cli/crypto/tlsutil" - "github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/jose" ) -// Minimum and maximum validity of an end-entity (not root or intermediate) certificate. -// They will be overwritten with the values configured in the authority -var ( - minCertDuration = 5 * time.Minute - maxCertDuration = 24 * time.Hour -) - -// Claim interface is implemented by types used to validate specific claims in a -// certificate request. -// TODO(mariano): Rename? -type Claim interface { - Valid(cr *x509.CertificateRequest) error -} - -// SignOptions contains the options that can be passed to the Authority.Sign -// method. -type SignOptions struct { - NotAfter time.Time `json:"notAfter"` - NotBefore time.Time `json:"notBefore"` -} - // Authority is the interface implemented by a CA authority. type Authority interface { - Authorize(ott string) ([]Claim, error) + Authorize(ott string) ([]interface{}, error) GetTLSOptions() *tlsutil.TLSOptions - GetMinDuration() time.Duration - GetMaxDuration() time.Duration Root(shasum string) (*x509.Certificate, error) - Sign(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error) + Sign(cr *x509.CertificateRequest, signOpts authority.SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error) Renew(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) - GetProvisioners() ([]*provisioner.Provisioner, error) + GetProvisioners() ([]*authority.Provisioner, error) GetEncryptedKey(kid string) (string, error) } @@ -176,7 +152,7 @@ type SignRequest struct { // ProvisionersResponse is the response object that returns the list of // provisioners. type ProvisionersResponse struct { - Provisioners []*provisioner.Provisioner `json:"provisioners"` + Provisioners []*authority.Provisioner `json:"provisioners"` } // JWKSetByIssuerResponse is the response object that returns the map of @@ -204,27 +180,6 @@ func (s *SignRequest) Validate() error { return BadRequest(errors.New("missing ott")) } - now := time.Now() - if s.NotBefore.IsZero() { - s.NotBefore = now - } - if s.NotAfter.IsZero() { - s.NotAfter = now.Add(x509util.DefaultCertValidity) - } - - if s.NotAfter.Before(now) { - return BadRequest(errors.New("notAfter < now")) - } - if s.NotAfter.Before(s.NotBefore) { - return BadRequest(errors.New("notAfter < notBefore")) - } - requestedDuration := s.NotAfter.Sub(s.NotBefore) - if requestedDuration < minCertDuration { - return BadRequest(errors.New("requested certificate validity duration is too short")) - } - if requestedDuration > maxCertDuration { - return BadRequest(errors.New("requested certificate validity duration is too long")) - } return nil } @@ -243,8 +198,6 @@ type caHandler struct { // New creates a new RouterHandler with the CA endpoints. func New(authority Authority) RouterHandler { - minCertDuration = authority.GetMinDuration() - maxCertDuration = authority.GetMaxDuration() return &caHandler{ Authority: authority, } @@ -296,18 +249,18 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { return } - claims, err := h.Authority.Authorize(body.OTT) + signOpts := authority.SignOptions{ + NotBefore: body.NotBefore, + NotAfter: body.NotAfter, + } + + extraOpts, err := h.Authority.Authorize(body.OTT) if err != nil { WriteError(w, Unauthorized(err)) return } - opts := SignOptions{ - NotBefore: body.NotBefore, - NotAfter: body.NotAfter, - } - - cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, claims...) + cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, signOpts, extraOpts...) if err != nil { WriteError(w, Forbidden(err)) return diff --git a/api/api_test.go b/api/api_test.go index 5b7863d2..f636f4e3 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -17,7 +17,7 @@ import ( "time" "github.com/go-chi/chi" - "github.com/smallstep/ca-component/provisioner" + "github.com/smallstep/ca-component/authority" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/jose" ) @@ -349,7 +349,6 @@ func TestCertificateRequest_UnmarshalJSON_json(t *testing.T) { } func TestSignRequest_Validate(t *testing.T) { - now := time.Now() csr := parseCertificateRequest(csrPEM) bad := parseCertificateRequest(csrPEM) bad.Signature[0]++ @@ -364,16 +363,9 @@ func TestSignRequest_Validate(t *testing.T) { fields fields wantErr bool }{ - {"ok", fields{CertificateRequest{csr}, "foobarzar", time.Time{}, time.Time{}}, false}, - {"ok 5m", fields{CertificateRequest{csr}, "foobarzar", now, now.Add(5 * time.Minute)}, false}, - {"ok 24h", fields{CertificateRequest{csr}, "foobarzar", now, now.Add(24 * time.Hour)}, false}, {"missing csr", fields{CertificateRequest{}, "foobarzar", time.Time{}, time.Time{}}, true}, {"invalid csr", fields{CertificateRequest{bad}, "foobarzar", time.Time{}, time.Time{}}, true}, {"missing ott", fields{CertificateRequest{csr}, "", time.Time{}, time.Time{}}, true}, - {"notAfter < now", fields{CertificateRequest{csr}, "foobarzar", now, now.Add(-5 * time.Minute)}, true}, - {"notAfter < notBefore", fields{CertificateRequest{csr}, "foobarzar", now.Add(5 * time.Minute), now.Add(4 * time.Minute)}, true}, - {"too short", fields{CertificateRequest{csr}, "foobarzar", now, now.Add(4 * time.Minute)}, true}, - {"too long", fields{CertificateRequest{csr}, "foobarzar", now, now.Add(24 * time.Hour).Add(1 * time.Minute)}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -393,20 +385,20 @@ func TestSignRequest_Validate(t *testing.T) { type mockAuthority struct { ret1, ret2 interface{} err error - authorize func(ott string) ([]Claim, error) + authorize func(ott string) ([]interface{}, error) getTLSOptions func() *tlsutil.TLSOptions root func(shasum string) (*x509.Certificate, error) - sign func(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error) + sign func(cr *x509.CertificateRequest, signOpts authority.SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error) renew func(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) - getProvisioners func() ([]*provisioner.Provisioner, error) + getProvisioners func() ([]*authority.Provisioner, error) getEncryptedKey func(kid string) (string, error) } -func (m *mockAuthority) Authorize(ott string) ([]Claim, error) { +func (m *mockAuthority) Authorize(ott string) ([]interface{}, error) { if m.authorize != nil { return m.authorize(ott) } - return m.ret1.([]Claim), m.err + return m.ret1.([]interface{}), m.err } func (m *mockAuthority) GetTLSOptions() *tlsutil.TLSOptions { @@ -416,14 +408,6 @@ func (m *mockAuthority) GetTLSOptions() *tlsutil.TLSOptions { return m.ret1.(*tlsutil.TLSOptions) } -func (m *mockAuthority) GetMinDuration() time.Duration { - return minCertDuration -} - -func (m *mockAuthority) GetMaxDuration() time.Duration { - return maxCertDuration -} - func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) { if m.root != nil { return m.root(shasum) @@ -431,9 +415,9 @@ func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) { return m.ret1.(*x509.Certificate), m.err } -func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error) { +func (m *mockAuthority) Sign(cr *x509.CertificateRequest, signOpts authority.SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error) { if m.sign != nil { - return m.sign(cr, opts, claims...) + return m.sign(cr, signOpts, extraOpts...) } return m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate), m.err } @@ -445,11 +429,11 @@ func (m *mockAuthority) Renew(cert *x509.Certificate) (*x509.Certificate, *x509. return m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate), m.err } -func (m *mockAuthority) GetProvisioners() ([]*provisioner.Provisioner, error) { +func (m *mockAuthority) GetProvisioners() ([]*authority.Provisioner, error) { if m.getProvisioners != nil { return m.getProvisioners() } - return m.ret1.([]*provisioner.Provisioner), m.err + return m.ret1.([]*authority.Provisioner), m.err } func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) { @@ -567,14 +551,14 @@ func Test_caHandler_Sign(t *testing.T) { } tests := []struct { - name string - input string - claims []Claim - autherr error - cert *x509.Certificate - root *x509.Certificate - signErr error - statusCode int + name string + input string + certAttrOpts []interface{} + autherr error + cert *x509.Certificate + root *x509.Certificate + signErr error + statusCode int }{ {"ok", string(valid), nil, nil, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated}, {"json read error", "{", nil, nil, nil, nil, nil, http.StatusBadRequest}, @@ -589,8 +573,8 @@ func Test_caHandler_Sign(t *testing.T) { t.Run(tt.name, func(t *testing.T) { h := New(&mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.signErr, - authorize: func(ott string) ([]Claim, error) { - return tt.claims, tt.autherr + authorize: func(ott string) ([]interface{}, error) { + return tt.certAttrOpts, tt.autherr }, getTLSOptions: func() *tlsutil.TLSOptions { return nil @@ -690,7 +674,7 @@ func Test_caHandler_JWKSetByIssuer(t *testing.T) { t.Fatal(err) } - p := []*provisioner.Provisioner{ + p := []*authority.Provisioner{ { Issuer: "p1", Key: &key, @@ -766,7 +750,7 @@ func Test_caHandler_Provisioners(t *testing.T) { t.Fatal(err) } - p := []*provisioner.Provisioner{ + p := []*authority.Provisioner{ { Type: "JWK", Issuer: "max", diff --git a/authority/authority_test.go b/authority/authority_test.go index b57108b6..c294967a 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -7,7 +7,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" - "github.com/smallstep/ca-component/provisioner" stepJOSE "github.com/smallstep/cli/jose" ) @@ -16,7 +15,7 @@ func testAuthority(t *testing.T) *Authority { assert.FatalError(t, err) clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk") assert.FatalError(t, err) - p := []*provisioner.Provisioner{ + p := []*Provisioner{ { Issuer: "Max", Type: "JWK", @@ -29,11 +28,11 @@ func testAuthority(t *testing.T) *Authority { }, } c := &Config{ - Address: "127.0.0.1", + Address: "127.0.0.1:443", Root: "testdata/secrets/root_ca.crt", IntermediateCert: "testdata/secrets/intermediate_ca.crt", IntermediateKey: "testdata/secrets/intermediate_ca_key", - DNSNames: []string{"test.smallstep.com"}, + DNSNames: []string{"test.ca.smallstep.com"}, Password: "pass", AuthorityConfig: &AuthConfig{ Provisioners: p, diff --git a/authority/authorize.go b/authority/authorize.go index 545590a3..7499d19b 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -5,8 +5,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/ca-component/api" - "github.com/smallstep/ca-component/provisioner" "gopkg.in/square/go-jose.v2/jwt" ) @@ -15,17 +13,19 @@ type idUsed struct { Subject string `json:"sub,omitempty"` } -func containsAtLeastOneAudience(claim []string, expected []string) bool { - if len(expected) == 0 { +// containsAtLeastOneAudience returns true if 'as' contains at least one element +// of 'bs', otherwise returns false. +func containsAtLeastOneAudience(as []string, bs []string) bool { + if len(bs) == 0 { return true } - if len(claim) == 0 { + if len(as) == 0 { return false } - for _, exp := range expected { - for _, cl := range claim { - if exp == cl { + for _, b := range bs { + for _, a := range as { + if b == a { return true } } @@ -35,35 +35,33 @@ func containsAtLeastOneAudience(claim []string, expected []string) bool { // Authorize authorizes a signature request by validating and authenticating // a OTT that must be sent w/ the request. -func (a *Authority) Authorize(ott string) ([]api.Claim, error) { +func (a *Authority) Authorize(ott string) ([]interface{}, error) { var ( errContext = map[string]interface{}{"ott": ott} claims = jwt.Claims{} - // Claims to check in the Sign method - downstreamClaims []api.Claim ) // Validate payload token, err := jwt.ParseSigned(ott) if err != nil { - return nil, &apiError{errors.Wrapf(err, "error parsing OTT to JSONWebToken"), + return nil, &apiError{errors.Wrapf(err, "authorize: error parsing token"), http.StatusUnauthorized, errContext} } kid := token.Headers[0].KeyID // JWT will only have 1 header. if len(kid) == 0 { - return nil, &apiError{errors.New("keyID cannot be empty"), + return nil, &apiError{errors.New("authorize: token KeyID cannot be empty"), http.StatusUnauthorized, errContext} } val, ok := a.provisionerIDIndex.Load(kid) if !ok { - return nil, &apiError{errors.Errorf("Provisioner with KeyID %s could not be found", kid), + return nil, &apiError{errors.Errorf("authorize: provisioner with KeyID %s not found", kid), http.StatusUnauthorized, errContext} } - p, ok := val.(*provisioner.Provisioner) + p, ok := val.(*Provisioner) if !ok { - return nil, &apiError{errors.Errorf("stored value is not a *Provisioner"), - http.StatusInternalServerError, context{}} + return nil, &apiError{errors.Errorf("authorize: invalid provisioner type"), + http.StatusInternalServerError, errContext} } if err = token.Claims(p.Key, &claims); err != nil { @@ -75,7 +73,7 @@ func (a *Authority) Authorize(ott string) ([]api.Claim, error) { if err = claims.ValidateWithLeeway(jwt.Expected{ Issuer: p.Issuer, }, time.Minute); err != nil { - return nil, &apiError{errors.Wrapf(err, "error validating OTT"), + return nil, &apiError{errors.Wrapf(err, "authorize: invalid token"), http.StatusUnauthorized, errContext} } @@ -89,17 +87,22 @@ func (a *Authority) Authorize(ott string) ([]api.Claim, error) { } if !containsAtLeastOneAudience(claims.Audience, a.audiences) { - return nil, &apiError{errors.New("invalid audience"), http.StatusUnauthorized, + return nil, &apiError{errors.New("authorize: token audience invalid"), http.StatusUnauthorized, errContext} } if claims.Subject == "" { - return nil, &apiError{errors.New("OTT sub cannot be empty"), + return nil, &apiError{errors.New("authorize: token subject cannot be empty"), http.StatusUnauthorized, errContext} } - downstreamClaims = append(downstreamClaims, &commonNameClaim{claims.Subject}) - downstreamClaims = append(downstreamClaims, &dnsNamesClaim{claims.Subject}) - downstreamClaims = append(downstreamClaims, &ipAddressesClaim{claims.Subject}) + + signOps := []interface{}{ + &commonNameClaim{claims.Subject}, + &dnsNamesClaim{claims.Subject}, + &ipAddressesClaim{claims.Subject}, + withIssuerAlternativeNameExtension(p.Issuer + ":" + p.Key.KeyID), + p, + } // Store the token to protect against reuse. if _, ok := a.ottMap.LoadOrStore(claims.ID, &idUsed{ @@ -110,5 +113,5 @@ func (a *Authority) Authorize(ott string) ([]api.Claim, error) { errContext} } - return downstreamClaims, nil + return signOps, nil } diff --git a/authority/authorize_test.go b/authority/authorize_test.go index cb627a5d..00d3b5b6 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -7,7 +7,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" - "github.com/smallstep/ca-component/api" "github.com/smallstep/cli/crypto/keys" stepJOSE "github.com/smallstep/cli/jose" jose "gopkg.in/square/go-jose.v2" @@ -27,53 +26,127 @@ func TestAuthorize(t *testing.T) { now := time.Now() validIssuer := "step-cli" + validAudience := []string{"https://test.ca.smallstep.com/sign"} type authorizeTest struct { - ott string - err *apiError - claims []api.Claim + auth *Authority + ott string + err *apiError + res []interface{} } tests := map[string]func(t *testing.T) *authorizeTest{ - "invalid-ott": func(t *testing.T) *authorizeTest { + "fail invalid ott": func(t *testing.T) *authorizeTest { return &authorizeTest{ - ott: "foo", - err: &apiError{errors.New("error parsing OTT"), + auth: a, + ott: "foo", + err: &apiError{errors.New("authorize: error parsing token"), http.StatusUnauthorized, context{"ott": "foo"}}, - claims: nil} + } }, - "invalid-issuer": func(t *testing.T) *authorizeTest { + "fail empty key id": func(t *testing.T) *authorizeTest { + _sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT")) + assert.FatalError(t, err) + cl := jwt.Claims{ + Subject: "test.smallstep.com", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: validAudience, + ID: "43", + } + raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + ott: raw, + err: &apiError{errors.New("authorize: token KeyID cannot be empty"), + http.StatusUnauthorized, context{"ott": raw}}, + } + }, + "fail provisioner not found": func(t *testing.T) *authorizeTest { + _sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "foo")) + assert.FatalError(t, err) + + cl := jwt.Claims{ + Subject: "test.smallstep.com", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: validAudience, + ID: "43", + } + raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + ott: raw, + err: &apiError{errors.New("authorize: provisioner with KeyID foo not found"), + http.StatusUnauthorized, context{"ott": raw}}, + } + }, + "fail invalid provisioner": func(t *testing.T) *authorizeTest { + _a := testAuthority(t) + + _sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "foo")) + assert.FatalError(t, err) + + _a.provisionerIDIndex.Store("foo", "42") + + cl := jwt.Claims{ + Subject: "test.smallstep.com", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: validAudience, + ID: "43", + } + raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: _a, + ott: raw, + err: &apiError{errors.New("authorize: invalid provisioner type"), + http.StatusInternalServerError, context{"ott": raw}}, + } + }, + "fail invalid issuer": func(t *testing.T) *authorizeTest { cl := jwt.Claims{ Subject: "subject", Issuer: "invalid-issuer", NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: validTokenAudience, + Audience: validAudience, } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - ott: raw, - err: &apiError{errors.New("error validating OTT"), + auth: a, + ott: raw, + err: &apiError{errors.New("authorize: invalid token"), http.StatusUnauthorized, context{"ott": raw}}, - claims: nil} + } }, - "empty-subject": func(t *testing.T) *authorizeTest { + "fail empty subject": func(t *testing.T) *authorizeTest { cl := jwt.Claims{ Subject: "", Issuer: validIssuer, NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: validTokenAudience, + Audience: validAudience, } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - ott: raw, - err: &apiError{errors.New("OTT sub cannot be empty"), + auth: a, + ott: raw, + err: &apiError{errors.New("authorize: token subject cannot be empty"), http.StatusUnauthorized, context{"ott": raw}}, - claims: nil} + } }, - "verify-sig-failure": func(t *testing.T) *authorizeTest { + "fail verify-sig-failure": func(t *testing.T) *authorizeTest { _, priv2, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) invalidKeySig, err := jose.NewSigner(jose.SigningKey{ @@ -82,27 +155,28 @@ func TestAuthorize(t *testing.T) { }, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) cl := jwt.Claims{ - Subject: "foo", + Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: validTokenAudience, + Audience: validAudience, } raw, err := jwt.Signed(invalidKeySig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - ott: raw, + auth: a, + ott: raw, err: &apiError{errors.New("square/go-jose: error in cryptographic primitive"), http.StatusUnauthorized, context{"ott": raw}}, - claims: nil} + } }, - "token-already-used": func(t *testing.T) *authorizeTest { + "fail token-already-used": func(t *testing.T) *authorizeTest { cl := jwt.Claims{ - Subject: "foo", + Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: validTokenAudience, + Audience: validAudience, ID: "42", } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() @@ -110,25 +184,31 @@ func TestAuthorize(t *testing.T) { _, err = a.Authorize(raw) assert.FatalError(t, err) return &authorizeTest{ - ott: raw, + auth: a, + ott: raw, err: &apiError{errors.New("token already used"), http.StatusUnauthorized, context{"ott": raw}}, - claims: nil} + } }, - "success": func(t *testing.T) *authorizeTest { + "ok": func(t *testing.T) *authorizeTest { cl := jwt.Claims{ - Subject: "foo", + Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: validTokenAudience, + Audience: validAudience, ID: "43", } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - ott: raw, - claims: []api.Claim{&commonNameClaim{"foo"}, &dnsNamesClaim{"foo"}, &ipAddressesClaim{"foo"}}, + auth: a, + ott: raw, + res: []interface{}{ + "1", "2", "3", + withIssuerAlternativeNameExtension("step-cli:" + jwk.KeyID), + "5", + }, } }, } @@ -138,7 +218,7 @@ func TestAuthorize(t *testing.T) { tc := genTestCase(t) assert.FatalError(t, err) - claims, err := a.Authorize(tc.ott) + crtOpts, err := tc.auth.Authorize(tc.ott) if err != nil { if assert.NotNil(t, tc.err) { switch v := err.(type) { @@ -152,7 +232,7 @@ func TestAuthorize(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Equals(t, claims, tc.claims) + assert.Equals(t, len(crtOpts), len(tc.res)) } } }) diff --git a/authority/claims.go b/authority/claims.go index 4fbfd627..4dbea684 100644 --- a/authority/claims.go +++ b/authority/claims.go @@ -1,18 +1,24 @@ package authority import ( - "crypto/x509" "net" + "time" "github.com/pkg/errors" - "github.com/smallstep/ca-component/api" + x509 "github.com/smallstep/cli/pkg/x509" ) +// certClaim interface is implemented by types used to validate specific claims in a +// certificate request. +type certClaim interface { + Valid(crt *x509.Certificate) error +} + // ValidateClaims returns nil if all the claims are validated, it will return // the first error if a claim fails. -func ValidateClaims(cr *x509.CertificateRequest, claims []api.Claim) (err error) { +func validateClaims(crt *x509.Certificate, claims []certClaim) (err error) { for _, c := range claims { - if err = c.Valid(cr); err != nil { + if err = c.Valid(crt); err != nil { return err } } @@ -25,12 +31,12 @@ type commonNameClaim struct { } // Valid checks that certificate request common name matches the one configured. -func (c *commonNameClaim) Valid(cr *x509.CertificateRequest) error { - if cr.Subject.CommonName == "" { +func (c *commonNameClaim) Valid(crt *x509.Certificate) error { + if crt.Subject.CommonName == "" { return errors.New("common name cannot be empty") } - if cr.Subject.CommonName != c.name { - return errors.Errorf("common name claim failed - got %s, want %s", cr.Subject.CommonName, c.name) + if crt.Subject.CommonName != c.name { + return errors.Errorf("common name claim failed - got %s, want %s", crt.Subject.CommonName, c.name) } return nil } @@ -40,11 +46,11 @@ type dnsNamesClaim struct { } // Valid checks that certificate request common name matches the one configured. -func (c *dnsNamesClaim) Valid(cr *x509.CertificateRequest) error { - if len(cr.DNSNames) == 0 { +func (c *dnsNamesClaim) Valid(crt *x509.Certificate) error { + if len(crt.DNSNames) == 0 { return nil } - for _, name := range cr.DNSNames { + for _, name := range crt.DNSNames { if name != c.name { return errors.Errorf("DNS names claim failed - got %s, want %s", name, c.name) } @@ -57,14 +63,14 @@ type ipAddressesClaim struct { } // Valid checks that certificate request common name matches the one configured. -func (c *ipAddressesClaim) Valid(cr *x509.CertificateRequest) error { - if len(cr.IPAddresses) == 0 { +func (c *ipAddressesClaim) Valid(crt *x509.Certificate) error { + if len(crt.IPAddresses) == 0 { return nil } // If it's an IP validate that only that ip is in IP addresses if requestedIP := net.ParseIP(c.name); requestedIP != nil { - for _, ip := range cr.IPAddresses { + for _, ip := range crt.IPAddresses { if !ip.Equal(requestedIP) { return errors.Errorf("IP addresses claim failed - got %s, want %s", ip, requestedIP) } @@ -72,5 +78,37 @@ func (c *ipAddressesClaim) Valid(cr *x509.CertificateRequest) error { return nil } - return errors.Errorf("IP addresses claim failed - got %v, want none", cr.IPAddresses) + return errors.Errorf("IP addresses claim failed - got %v, want none", crt.IPAddresses) +} + +// certTemporalClaim validates the certificate temporal validity settings. +type certTemporalClaim struct { + min time.Duration + max time.Duration +} + +// Validate validates the certificate temporal validity settings. +func (ctc *certTemporalClaim) Valid(crt *x509.Certificate) error { + var ( + na = crt.NotAfter + nb = crt.NotBefore + d = na.Sub(nb) + now = time.Now() + ) + + if na.Before(now) { + return errors.Errorf("NotAfter: %v cannot be in the past", na) + } + if na.Before(nb) { + return errors.Errorf("NotAfter: %v cannot be before NotBefore: %v", na, nb) + } + if d < ctc.min { + return errors.Errorf("requested duration of %v is less than the authorized minimum certificate duration of %v", + d, ctc.min) + } + if d > ctc.max { + return errors.Errorf("requested duration of %v is more than the authorized maximum certificate duration of %v", + d, ctc.max) + } + return nil } diff --git a/authority/claims_test.go b/authority/claims_test.go index 7874f8f8..02a327cc 100644 --- a/authority/claims_test.go +++ b/authority/claims_test.go @@ -1,35 +1,34 @@ package authority import ( - "crypto/x509" "crypto/x509/pkix" "net" "testing" "github.com/pkg/errors" "github.com/smallstep/assert" - "github.com/smallstep/ca-component/api" + x509 "github.com/smallstep/cli/pkg/x509" ) func TestCommonNameClaim_Valid(t *testing.T) { tests := map[string]struct { - cnc api.Claim - crt *x509.CertificateRequest + cnc certClaim + crt *x509.Certificate err error }{ "empty-common-name": { cnc: &commonNameClaim{name: "foo"}, - crt: &x509.CertificateRequest{}, + crt: &x509.Certificate{}, err: errors.New("common name cannot be empty"), }, "wrong-common-name": { cnc: &commonNameClaim{name: "foo"}, - crt: &x509.CertificateRequest{Subject: pkix.Name{CommonName: "bar"}}, + crt: &x509.Certificate{Subject: pkix.Name{CommonName: "bar"}}, err: errors.New("common name claim failed - got bar, want foo"), }, "ok": { cnc: &commonNameClaim{name: "foo"}, - crt: &x509.CertificateRequest{Subject: pkix.Name{CommonName: "foo"}}, + crt: &x509.Certificate{Subject: pkix.Name{CommonName: "foo"}}, }, } @@ -49,27 +48,27 @@ func TestCommonNameClaim_Valid(t *testing.T) { func TestIPAddressesClaim_Valid(t *testing.T) { tests := map[string]struct { - iac api.Claim - crt *x509.CertificateRequest + iac certClaim + crt *x509.Certificate err error }{ "unexpected-ip": { iac: &ipAddressesClaim{name: "127.0.0.1"}, - crt: &x509.CertificateRequest{IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("1.1.1.1")}}, + crt: &x509.Certificate{IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("1.1.1.1")}}, err: errors.New("IP addresses claim failed - got 1.1.1.1, want 127.0.0.1"), }, "invalid-matcher-nonempty-ips": { iac: &ipAddressesClaim{name: "invalid"}, - crt: &x509.CertificateRequest{IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}}, + crt: &x509.Certificate{IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}}, err: errors.New("IP addresses claim failed - got [127.0.0.1], want none"), }, "ok": { iac: &ipAddressesClaim{name: "127.0.0.1"}, - crt: &x509.CertificateRequest{IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}}, + crt: &x509.Certificate{IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}}, }, "ok-empty-ips": { iac: &ipAddressesClaim{name: "127.0.0.1"}, - crt: &x509.CertificateRequest{IPAddresses: []net.IP{}}, + crt: &x509.Certificate{IPAddresses: []net.IP{}}, }, } @@ -89,26 +88,26 @@ func TestIPAddressesClaim_Valid(t *testing.T) { func TestDNSNamesClaim_Valid(t *testing.T) { tests := map[string]struct { - dnc api.Claim - crt *x509.CertificateRequest + dnc certClaim + crt *x509.Certificate err error }{ "wrong-dns-name": { dnc: &dnsNamesClaim{name: "foo"}, - crt: &x509.CertificateRequest{DNSNames: []string{"foo", "bar"}}, + crt: &x509.Certificate{DNSNames: []string{"foo", "bar"}}, err: errors.New("DNS names claim failed - got bar, want foo"), }, "ok": { dnc: &dnsNamesClaim{name: "foo"}, - crt: &x509.CertificateRequest{DNSNames: []string{"foo"}}, + crt: &x509.Certificate{DNSNames: []string{"foo"}}, }, "ok-empty-dnsNames": { dnc: &dnsNamesClaim{"foo"}, - crt: &x509.CertificateRequest{}, + crt: &x509.Certificate{}, }, "ok-multiple-identical-dns-entries": { dnc: &dnsNamesClaim{name: "foo"}, - crt: &x509.CertificateRequest{DNSNames: []string{"foo", "foo", "foo"}}, + crt: &x509.Certificate{DNSNames: []string{"foo", "foo", "foo"}}, }, } diff --git a/authority/config.go b/authority/config.go index 11f6c21e..23b5df51 100644 --- a/authority/config.go +++ b/authority/config.go @@ -6,35 +6,43 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/ca-component/provisioner" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" ) -// DefaultTLSOptions represents the default TLS version as well as the cipher -// suites used in the TLS certificates. -var DefaultTLSOptions = tlsutil.TLSOptions{ - CipherSuites: x509util.CipherSuites{ - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - }, - MinVersion: 1.2, - MaxVersion: 1.2, - Renegotiation: false, -} - -const ( - // minCertDuration is the minimum validity of an end-entity (not root or intermediate) certificate. - minCertDuration = 5 * time.Minute - // maxCertDuration is the maximum validity of an end-entity (not root or intermediate) certificate. - maxCertDuration = 24 * time.Hour +var ( + // DefaultTLSOptions represents the default TLS version as well as the cipher + // suites used in the TLS certificates. + DefaultTLSOptions = tlsutil.TLSOptions{ + CipherSuites: x509util.CipherSuites{ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + }, + MinVersion: 1.2, + MaxVersion: 1.2, + Renegotiation: false, + } + globalProvisionerClaims = ProvisionerClaims{ + MinTLSDur: &duration{5 * time.Minute}, + MaxTLSDur: &duration{24 * time.Hour}, + DefaultTLSDur: &duration{24 * time.Hour}, + } ) type duration struct { time.Duration } +// MarshalJSON parses a duration string and sets it to the duration. +// +// A duration string is a possibly signed sequence of decimal numbers, each with +// optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +func (d *duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + // UnmarshalJSON parses a duration string and sets it to the duration. // // A duration string is a possibly signed sequence of decimal numbers, each with @@ -67,24 +75,27 @@ type Config struct { // AuthConfig represents the configuration options for the authority. type AuthConfig struct { - Provisioners []*provisioner.Provisioner `json:"provisioners,omitempty"` - Template *x509util.ASN1DN `json:"template,omitempty"` - MinCertDuration *duration `json:"minCertDuration,omitempty"` - MaxCertDuration *duration `json:"maxCertDuration,omitempty"` - DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"` + Provisioners []*Provisioner `json:"provisioners,omitempty"` + Template *x509util.ASN1DN `json:"template,omitempty"` + Claims *ProvisionerClaims } // Validate validates the authority configuration. func (c *AuthConfig) Validate() error { + var err error + if c == nil { return errors.New("authority cannot be undefined") } if len(c.Provisioners) == 0 { return errors.New("authority.provisioners cannot be empty") } + + if c.Claims, err = c.Claims.Init(&globalProvisionerClaims); err != nil { + return err + } for _, p := range c.Provisioners { - err := p.Validate() - if err != nil { + if err := p.Init(c.Claims); err != nil { return err } } diff --git a/authority/config_test.go b/authority/config_test.go index 05ee450f..421275ef 100644 --- a/authority/config_test.go +++ b/authority/config_test.go @@ -5,7 +5,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" - "github.com/smallstep/ca-component/provisioner" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" stepJOSE "github.com/smallstep/cli/jose" @@ -18,7 +17,7 @@ func TestConfigValidate(t *testing.T) { clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk") assert.FatalError(t, err) ac := &AuthConfig{ - Provisioners: []*provisioner.Provisioner{ + Provisioners: []*Provisioner{ { Issuer: "Max", Type: "JWK", @@ -216,7 +215,7 @@ func TestAuthConfigValidate(t *testing.T) { assert.FatalError(t, err) clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk") assert.FatalError(t, err) - p := []*provisioner.Provisioner{ + p := []*Provisioner{ { Issuer: "Max", Type: "JWK", @@ -250,7 +249,7 @@ func TestAuthConfigValidate(t *testing.T) { "fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest { return AuthConfigValidateTest{ ac: &AuthConfig{ - Provisioners: []*provisioner.Provisioner{ + Provisioners: []*Provisioner{ {Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}}, {Issuer: "foo", Key: &jose.JSONWebKey{}}, }, diff --git a/authority/provisioner.go b/authority/provisioner.go new file mode 100644 index 00000000..21fa23bc --- /dev/null +++ b/authority/provisioner.go @@ -0,0 +1,133 @@ +package authority + +import ( + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/x509util" + + jose "gopkg.in/square/go-jose.v2" +) + +// baseClaims interface. +type baseClaims struct { + MinTLSDur *duration `json:"minTLSCertDuration,omitempty"` + MaxTLSDur *duration `json:"maxTLSCertDuration,omitempty"` + DefaultTLSDur *duration `json:"defaultTLSCertDuration,omitempty"` +} + +// ProvisionerClaims so that individual provisioners can override global claims. +type ProvisionerClaims struct { + globalClaims *ProvisionerClaims + MinTLSDur *duration `json:"minTLSCertDuration,omitempty"` + MaxTLSDur *duration `json:"maxTLSCertDuration,omitempty"` + DefaultTLSDur *duration `json:"defaultTLSCertDuration,omitempty"` +} + +// Init initializes and validates the individual provisioner claims. +func (pc *ProvisionerClaims) Init(global *ProvisionerClaims) (*ProvisionerClaims, error) { + if pc == nil { + pc = &ProvisionerClaims{} + } + pc.globalClaims = global + err := pc.Validate() + return pc, err +} + +// DefaultTLSCertDuration returns the default TLS cert duration for the +// provisioner. If the default is not set within the provisioner, then the global +// default from the authority configuration will be used. +func (pc *ProvisionerClaims) DefaultTLSCertDuration() time.Duration { + if pc.DefaultTLSDur == nil || pc.DefaultTLSDur.Duration == 0 { + return pc.globalClaims.DefaultTLSCertDuration() + } + return pc.DefaultTLSDur.Duration +} + +// MinTLSCertDuration returns the minimum TLS cert duration for the provisioner. +// If the minimum is not set within the provisioner, then the global +// minimum from the authority configuration will be used. +func (pc *ProvisionerClaims) MinTLSCertDuration() time.Duration { + if pc.MinTLSDur == nil || pc.MinTLSDur.Duration == 0 { + return pc.globalClaims.MinTLSCertDuration() + } + return pc.MinTLSDur.Duration +} + +// MaxTLSCertDuration returns the maximum TLS cert duration for the provisioner. +// If the maximum is not set within the provisioner, then the global +// maximum from the authority configuration will be used. +func (pc *ProvisionerClaims) MaxTLSCertDuration() time.Duration { + if pc.MaxTLSDur == nil || pc.MaxTLSDur.Duration == 0 { + return pc.globalClaims.MaxTLSCertDuration() + } + return pc.MaxTLSDur.Duration +} + +// Validate validates and modifies the Claims with default values. +func (pc *ProvisionerClaims) Validate() error { + var ( + min = pc.MinTLSCertDuration() + max = pc.MaxTLSCertDuration() + def = pc.DefaultTLSCertDuration() + ) + switch { + case min == 0: + return errors.Errorf("claims: MinTLSCertDuration cannot be empty") + case max == 0: + return errors.Errorf("claims: MaxTLSCertDuration cannot be empty") + case def == 0: + return errors.Errorf("claims: DefaultTLSCertDuration cannot be empty") + case max < min: + return errors.Errorf("claims: MaxCertDuration cannot be less "+ + "than MinCertDuration: MaxCertDuration - %v, MinCertDuration - %v", max, min) + case def < min: + return errors.Errorf("claims: DefaultCertDuration cannot be less than MinCertDuration: DefaultCertDuration - %v, MinCertDuration - %v", def, min) + case max < def: + return errors.Errorf("claims: MaxCertDuration cannot be less than DefaultCertDuration: MaxCertDuration - %v, DefaultCertDuration - %v", max, def) + default: + return nil + } +} + +// Provisioner - authorized entity that can sign tokens necessary for signature requests. +type Provisioner struct { + Issuer string `json:"issuer,omitempty"` + Type string `json:"type,omitempty"` + Key *jose.JSONWebKey `json:"key,omitempty"` + EncryptedKey string `json:"encryptedKey,omitempty"` + Claims *ProvisionerClaims `json:"claims,omitempty"` +} + +// Init initializes and validates a the fields of Provisioner type. +func (p *Provisioner) Init(global *ProvisionerClaims) error { + switch { + case p.Issuer == "": + return errors.New("provisioner issuer cannot be empty") + + case p.Type == "": + return errors.New("provisioner type cannot be empty") + + case p.Key == nil: + return errors.New("provisioner key cannot be empty") + } + + var err error + p.Claims, err = p.Claims.Init(global) + return err +} + +// getTLSApps returns a list of modifiers and validators that will be applied to +// the certificate. +func (p *Provisioner) getTLSApps(so SignOptions) ([]x509util.WithOption, []certClaim, error) { + c := p.Claims + return []x509util.WithOption{ + x509util.WithNotBeforeAfterDuration(so.NotBefore, + so.NotAfter, c.DefaultTLSCertDuration()), + }, []certClaim{ + &certTemporalClaim{ + min: c.MinTLSCertDuration(), + max: c.MaxTLSCertDuration(), + }, + }, nil +} diff --git a/provisioner/provisioner_test.go b/authority/provisioner_test.go similarity index 92% rename from provisioner/provisioner_test.go rename to authority/provisioner_test.go index 8f1e6f18..50d84127 100644 --- a/provisioner/provisioner_test.go +++ b/authority/provisioner_test.go @@ -1,4 +1,4 @@ -package provisioner +package authority import ( "errors" @@ -8,7 +8,7 @@ import ( jose "gopkg.in/square/go-jose.v2" ) -func TestProvisionerValidate(t *testing.T) { +func TestProvisionerInit(t *testing.T) { type ProvisionerValidateTest struct { p *Provisioner err error @@ -42,7 +42,7 @@ func TestProvisionerValidate(t *testing.T) { for name, get := range tests { t.Run(name, func(t *testing.T) { tc := get(t) - err := tc.p.Validate() + err := tc.p.Init(&globalProvisionerClaims) if err != nil { if assert.NotNil(t, tc.err) { assert.Equals(t, tc.err.Error(), err.Error()) diff --git a/authority/provisioners.go b/authority/provisioners.go index cfcdb738..89f6b253 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/pkg/errors" - "github.com/smallstep/ca-component/provisioner" ) // GetEncryptedKey returns the JWE key corresponding to the given kid argument. @@ -25,6 +24,6 @@ func (a *Authority) GetEncryptedKey(kid string) (string, error) { // GetProvisioners returns a map listing each provisioner and the JWK Key Set // with their public keys. -func (a *Authority) GetProvisioners() ([]*provisioner.Provisioner, error) { +func (a *Authority) GetProvisioners() ([]*Provisioner, error) { return a.config.AuthorityConfig.Provisioners, nil } diff --git a/authority/provisioners_test.go b/authority/provisioners_test.go index 40efa318..973a59f6 100644 --- a/authority/provisioners_test.go +++ b/authority/provisioners_test.go @@ -6,7 +6,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" - "github.com/smallstep/ca-component/provisioner" ) func TestGetEncryptedKey(t *testing.T) { @@ -73,7 +72,7 @@ func TestGetEncryptedKey(t *testing.T) { if assert.Nil(t, tc.err) { val, ok := tc.a.provisionerIDIndex.Load(tc.kid) assert.Fatal(t, ok) - p, ok := val.(*provisioner.Provisioner) + p, ok := val.(*Provisioner) assert.Fatal(t, ok) assert.Equals(t, p.EncryptedKey, ek) } diff --git a/authority/tls.go b/authority/tls.go index c2eda620..29572edd 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -3,42 +3,58 @@ package authority import ( "crypto/tls" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "net/http" "strings" "time" "github.com/pkg/errors" - "github.com/smallstep/ca-component/api" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" stepx509 "github.com/smallstep/cli/pkg/x509" ) -// GetMinDuration returns the minimum validity of an end-entity (not root or -// intermediate) certificate. -func (a *Authority) GetMinDuration() time.Duration { - if a.config.AuthorityConfig.MinCertDuration == nil { - return minCertDuration - } - return a.config.AuthorityConfig.MinCertDuration.Duration -} - -// GetMaxDuration returns the maximum validity of an end-entity (not root or -// intermediate) certificate. -func (a *Authority) GetMaxDuration() time.Duration { - if a.config.AuthorityConfig.MaxCertDuration == nil { - return maxCertDuration - } - return a.config.AuthorityConfig.MaxCertDuration.Duration -} - // GetTLSOptions returns the tls options configured. func (a *Authority) GetTLSOptions() *tlsutil.TLSOptions { return a.config.TLS } +// SignOptions contains the options that can be passed to the Authority.Sign +// method. +type SignOptions struct { + NotAfter time.Time `json:"notAfter"` + NotBefore time.Time `json:"notBefore"` +} + +func withIssuerAlternativeNameExtension(name string) x509util.WithOption { + return func(p x509util.Profile) error { + crt := p.Subject() + + iatExt := []asn1.RawValue{ + asn1.RawValue{ + Tag: 2, + Class: 2, + Bytes: []byte(name), + }, + } + iatExtBytes, err := asn1.Marshal(iatExt) + if err != nil { + return &apiError{err, http.StatusInternalServerError, nil} + } + + crt.ExtraExtensions = append(crt.ExtraExtensions, pkix.Extension{ + Id: []int{2, 5, 9, 18}, + Critical: false, + Value: iatExtBytes, + }) + + return nil + } +} + func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption { return func(p x509util.Profile) error { if def == nil { @@ -70,17 +86,40 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption { } // Sign creates a signed certificate from a certificate signing request. -func (a *Authority) Sign(csr *x509.CertificateRequest, opts api.SignOptions, claims ...api.Claim) (*x509.Certificate, *x509.Certificate, error) { - if err := ValidateClaims(csr, claims); err != nil { - return nil, nil, &apiError{err, http.StatusUnauthorized, context{}} +func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error) { + var ( + errContext = context{"csr": csr, "signOptions": signOpts} + claims = []certClaim{} + mods = []x509util.WithOption{} + ) + for _, op := range extraOpts { + switch k := op.(type) { + case certClaim: + claims = append(claims, k) + case x509util.WithOption: + mods = append(mods, k) + case *Provisioner: + m, c, err := k.getTLSApps(signOpts) + if err != nil { + return nil, nil, &apiError{err, http.StatusInternalServerError, errContext} + } + mods = append(mods, m...) + mods = append(mods, []x509util.WithOption{ + x509util.WithHosts(csr.Subject.CommonName), + withDefaultASN1DN(a.config.AuthorityConfig.Template), + }...) + claims = append(claims, c...) + default: + return nil, nil, &apiError{errors.Errorf("sign: invalid extra option type %T", k), + http.StatusInternalServerError, errContext} + } } stepCSR, err := stepx509.ParseCertificateRequest(csr.Raw) if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "error converting x509 csr to stepx509 csr"), - http.StatusInternalServerError, context{}} + return nil, nil, &apiError{errors.Wrap(err, "sign: error converting x509 csr to stepx509 csr"), + http.StatusInternalServerError, errContext} } - // DNSNames and IPAddresses are validated but to avoid duplications we will // clean them as x509util.NewLeafProfileWithCSR will set the right values. stepCSR.DNSNames = nil @@ -88,27 +127,31 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, opts api.SignOptions, cla issIdentity := a.intermediateIdentity leaf, err := x509util.NewLeafProfileWithCSR(stepCSR, issIdentity.Crt, - issIdentity.Key, x509util.WithHosts(csr.Subject.CommonName), - x509util.WithNotBeforeAfter(opts.NotBefore, opts.NotAfter), - withDefaultASN1DN(a.config.AuthorityConfig.Template)) + issIdentity.Key, mods...) if err != nil { - return nil, nil, &apiError{err, http.StatusInternalServerError, context{}} + return nil, nil, &apiError{errors.Wrapf(err, "sign"), http.StatusInternalServerError, errContext} } + + if err := validateClaims(leaf.Subject(), claims); err != nil { + return nil, nil, &apiError{errors.Wrapf(err, "sign"), http.StatusUnauthorized, errContext} + } + crtBytes, err := leaf.CreateCertificate() if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "error creating new leaf certificate from input csr"), - http.StatusInternalServerError, context{}} + return nil, nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"), + http.StatusInternalServerError, errContext} } serverCert, err := x509.ParseCertificate(crtBytes) if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "error parsing new server certificate"), - http.StatusInternalServerError, context{}} + return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"), + http.StatusInternalServerError, errContext} } + caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "error parsing intermediate certificate"), - http.StatusInternalServerError, context{}} + return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"), + http.StatusInternalServerError, errContext} } return serverCert, caCert, nil @@ -143,15 +186,15 @@ func (a *Authority) Renew(ocx *x509.Certificate) (*x509.Certificate, *x509.Certi ExtKeyUsage: oldCert.ExtKeyUsage, UnknownExtKeyUsage: oldCert.UnknownExtKeyUsage, BasicConstraintsValid: oldCert.BasicConstraintsValid, - IsCA: oldCert.IsCA, - MaxPathLen: oldCert.MaxPathLen, - MaxPathLenZero: oldCert.MaxPathLenZero, - OCSPServer: oldCert.OCSPServer, - IssuingCertificateURL: oldCert.IssuingCertificateURL, - DNSNames: oldCert.DNSNames, - EmailAddresses: oldCert.EmailAddresses, - IPAddresses: oldCert.IPAddresses, - URIs: oldCert.URIs, + IsCA: oldCert.IsCA, + MaxPathLen: oldCert.MaxPathLen, + MaxPathLenZero: oldCert.MaxPathLenZero, + OCSPServer: oldCert.OCSPServer, + IssuingCertificateURL: oldCert.IssuingCertificateURL, + DNSNames: oldCert.DNSNames, + EmailAddresses: oldCert.EmailAddresses, + IPAddresses: oldCert.IPAddresses, + URIs: oldCert.URIs, PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical, PermittedDNSDomains: oldCert.PermittedDNSDomains, ExcludedDNSDomains: oldCert.ExcludedDNSDomains, diff --git a/authority/tls_test.go b/authority/tls_test.go index 7db25df6..d8ed7382 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -12,7 +12,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" - "github.com/smallstep/ca-component/api" "github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" @@ -20,7 +19,7 @@ import ( func getCSR(t *testing.T, priv interface{}) *x509.CertificateRequest { _csr := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: "test"}, + Subject: pkix.Name{CommonName: "test.smallstep.com"}, DNSNames: []string{"test.smallstep.com"}, } csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv) @@ -42,90 +41,121 @@ func TestSign(t *testing.T) { Locality: "Landscapes", Province: "Sudden Cliffs", StreetAddress: "TNT", - CommonName: "test", + CommonName: "test.smallstep.com", } - now := time.Now() + nb := time.Now() + signOpts := SignOptions{ + NotBefore: nb, + NotAfter: nb.Add(time.Minute * 5), + } type signTest struct { - auth *Authority - csr *x509.CertificateRequest - opts api.SignOptions - claims []api.Claim - err *apiError + auth *Authority + csr *x509.CertificateRequest + signOpts SignOptions + extraOpts []interface{} + err *apiError } tests := map[string]func(*testing.T) *signTest{ - "fail-validate-claims": func(t *testing.T) *signTest { - csr := getCSR(t, priv) - return &signTest{ - auth: a, - csr: csr, - opts: api.SignOptions{ - NotBefore: now, - NotAfter: now.Add(time.Minute * 5), - }, - claims: []api.Claim{&commonNameClaim{"foo"}}, - err: &apiError{errors.New("common name claim failed - got test, want foo"), - http.StatusUnauthorized, context{}}, - } - }, - "fail-convert-stepCSR": func(t *testing.T) *signTest { + "fail invalid extra option": func(t *testing.T) *signTest { csr := getCSR(t, priv) csr.Raw = []byte("foo") return &signTest{ auth: a, csr: csr, - opts: api.SignOptions{ - NotBefore: now, - NotAfter: now.Add(time.Minute * 5), + extraOpts: []interface{}{ + withIssuerAlternativeNameExtension("baz"), + "42", + }, + signOpts: signOpts, + err: &apiError{errors.New("sign: invalid extra option type string"), + http.StatusInternalServerError, + context{"csr": csr, "signOptions": signOpts}, }, - claims: []api.Claim{&commonNameClaim{"test"}}, - err: &apiError{errors.New("error converting x509 csr to stepx509 csr"), - http.StatusInternalServerError, context{}}, } }, - "fail-merge-default-ASN1DN": func(t *testing.T) *signTest { + "fail convert csr to step format": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + csr.Raw = []byte("foo") + return &signTest{ + auth: a, + csr: csr, + extraOpts: []interface{}{ + withIssuerAlternativeNameExtension("baz"), + }, + signOpts: signOpts, + err: &apiError{errors.New("sign: error converting x509 csr to stepx509 csr"), + http.StatusInternalServerError, + context{"csr": csr, "signOptions": signOpts}, + }, + } + }, + "fail merge default ASN1DN": func(t *testing.T) *signTest { _a := testAuthority(t) _a.config.AuthorityConfig.Template = nil csr := getCSR(t, priv) return &signTest{ auth: _a, csr: csr, - opts: api.SignOptions{ - NotBefore: now, - NotAfter: now.Add(time.Minute * 5), + extraOpts: []interface{}{ + withIssuerAlternativeNameExtension("baz"), + a.config.AuthorityConfig.Provisioners[1], + }, + signOpts: signOpts, + err: &apiError{errors.New("sign: default ASN1DN template cannot be nil"), + http.StatusInternalServerError, + context{"csr": csr, "signOptions": signOpts}, }, - claims: []api.Claim{&commonNameClaim{"test"}}, - err: &apiError{errors.New("default ASN1DN template cannot be nil"), - http.StatusInternalServerError, context{}}, } }, - "fail-create-cert": func(t *testing.T) *signTest { + "fail create cert": func(t *testing.T) *signTest { _a := testAuthority(t) _a.intermediateIdentity.Key = nil csr := getCSR(t, priv) return &signTest{ auth: _a, csr: csr, - opts: api.SignOptions{ - NotBefore: now, - NotAfter: now.Add(time.Minute * 5), + extraOpts: []interface{}{ + withIssuerAlternativeNameExtension("baz"), + }, + signOpts: signOpts, + err: &apiError{errors.New("sign: error creating new leaf certificate"), + http.StatusInternalServerError, + context{"csr": csr, "signOptions": signOpts}, }, - claims: []api.Claim{&commonNameClaim{"test"}}, - err: &apiError{errors.New("error creating new leaf certificate from input csr"), - http.StatusInternalServerError, context{}}, } }, - "success": func(t *testing.T) *signTest { + "fail provisioner duration claim": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + _signOpts := SignOptions{ + NotBefore: nb, + NotAfter: nb.Add(time.Hour * 25), + } + return &signTest{ + auth: a, + csr: csr, + extraOpts: []interface{}{ + withIssuerAlternativeNameExtension("baz"), + a.config.AuthorityConfig.Provisioners[1], + }, + signOpts: _signOpts, + err: &apiError{errors.New("sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s"), + http.StatusUnauthorized, + context{"csr": csr, "signOptions": _signOpts}, + }, + } + }, + "ok": func(t *testing.T) *signTest { csr := getCSR(t, priv) return &signTest{ auth: a, csr: csr, - opts: api.SignOptions{ - NotBefore: now, - NotAfter: now.Add(time.Minute * 5), + extraOpts: []interface{}{ + withIssuerAlternativeNameExtension("baz"), + a.config.AuthorityConfig.Provisioners[1], }, - claims: []api.Claim{&commonNameClaim{"test"}}, + signOpts: signOpts, } }, } @@ -134,7 +164,7 @@ func TestSign(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - leaf, intermediate, err := tc.auth.Sign(tc.csr, tc.opts, tc.claims...) + leaf, intermediate, err := tc.auth.Sign(tc.csr, tc.signOpts, tc.extraOpts...) if err != nil { if assert.NotNil(t, tc.err) { switch v := err.(type) { @@ -148,8 +178,8 @@ func TestSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Equals(t, leaf.NotBefore, tc.opts.NotBefore.UTC().Truncate(time.Second)) - assert.Equals(t, leaf.NotAfter, tc.opts.NotAfter.UTC().Truncate(time.Second)) + assert.Equals(t, leaf.NotBefore, signOpts.NotBefore.UTC().Truncate(time.Second)) + assert.Equals(t, leaf.NotAfter, signOpts.NotAfter.UTC().Truncate(time.Second)) tmplt := a.config.AuthorityConfig.Template assert.Equals(t, fmt.Sprintf("%v", leaf.Subject), fmt.Sprintf("%v", &pkix.Name{ @@ -166,7 +196,7 @@ func TestSign(t *testing.T) { assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA) assert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) - assert.Equals(t, leaf.DNSNames, []string{"test"}) + assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"}) pubBytes, err := x509.MarshalPKIXPublicKey(pub) assert.FatalError(t, err) @@ -201,14 +231,14 @@ func TestRenew(t *testing.T) { now := time.Now().UTC() nb1 := now.Add(-time.Minute * 7) na1 := now - so := &api.SignOptions{ + so := &SignOptions{ NotBefore: nb1, NotAfter: na1, } leaf, err := x509util.NewLeafProfile("renew", a.intermediateIdentity.Crt, a.intermediateIdentity.Key, - x509util.WithNotBeforeAfter(so.NotBefore, so.NotAfter), + x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, 0), withDefaultASN1DN(a.config.AuthorityConfig.Template), x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test")) assert.FatalError(t, err) @@ -314,62 +344,6 @@ func TestRenew(t *testing.T) { } } -func TestGetMinDuration(t *testing.T) { - type renewTest struct { - auth *Authority - d time.Duration - } - tests := map[string]func() (*renewTest, error){ - "default": func() (*renewTest, error) { - a := testAuthority(t) - return &renewTest{auth: a, d: time.Minute * 5}, nil - }, - "non-default": func() (*renewTest, error) { - a := testAuthority(t) - a.config.AuthorityConfig.MinCertDuration = &duration{time.Minute * 7} - return &renewTest{auth: a, d: time.Minute * 7}, nil - }, - } - - for name, genTestCase := range tests { - t.Run(name, func(t *testing.T) { - tc, err := genTestCase() - assert.FatalError(t, err) - - d := tc.auth.GetMinDuration() - assert.Equals(t, d, tc.d) - }) - } -} - -func TestGetMaxDuration(t *testing.T) { - type renewTest struct { - auth *Authority - d time.Duration - } - tests := map[string]func() (*renewTest, error){ - "default": func() (*renewTest, error) { - a := testAuthority(t) - return &renewTest{auth: a, d: time.Hour * 24}, nil - }, - "non-default": func() (*renewTest, error) { - a := testAuthority(t) - a.config.AuthorityConfig.MaxCertDuration = &duration{time.Minute * 7} - return &renewTest{auth: a, d: time.Minute * 7}, nil - }, - } - - for name, genTestCase := range tests { - t.Run(name, func(t *testing.T) { - tc, err := genTestCase() - assert.FatalError(t, err) - - d := tc.auth.GetMaxDuration() - assert.Equals(t, d, tc.d) - }) - } -} - func TestGetTLSOptions(t *testing.T) { type renewTest struct { auth *Authority diff --git a/ca/ca_test.go b/ca/ca_test.go index 0c7e17e8..ac7fcd6f 100644 --- a/ca/ca_test.go +++ b/ca/ca_test.go @@ -75,10 +75,10 @@ func TestCASign(t *testing.T) { clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_priv.jwk", stepJOSE.WithPassword([]byte("pass"))) assert.FatalError(t, err) - fmt.Printf("clijwk.KeyID = %+v\n", clijwk.KeyID) sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: clijwk.Key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", clijwk.KeyID)) assert.FatalError(t, err) + validAud := []string{"https://127.0.0.1:0/sign"} now := time.Now().UTC() leafExpiry := now.Add(time.Minute * 5) @@ -90,7 +90,7 @@ func TestCASign(t *testing.T) { errMsg string } tests := map[string]func(t *testing.T) *signTest{ - "invalid-json-body": func(t *testing.T) *signTest { + "fail invalid-json-body": func(t *testing.T) *signTest { return &signTest{ ca: ca, body: "invalid json", @@ -98,7 +98,7 @@ func TestCASign(t *testing.T) { errMsg: "Bad Request", } }, - "invalid-csr-sig": func(t *testing.T) *signTest { + "fail invalid-csr-sig": func(t *testing.T) *signTest { der := []byte(`-----BEGIN CERTIFICATE REQUEST----- MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl @@ -136,7 +136,7 @@ ZEp7knvU2psWRw== errMsg: "Bad Request", } }, - "unauthorized-ott": func(t *testing.T) *signTest { + "fail unauthorized-ott": func(t *testing.T) *signTest { csr, err := getCSR(priv) assert.FatalError(t, err) body, err := json.Marshal(&api.SignRequest{ @@ -151,7 +151,7 @@ ZEp7knvU2psWRw== errMsg: "Unauthorized", } }, - "fail-commonname-claim": func(t *testing.T) *signTest { + "fail commonname-claim": func(t *testing.T) *signTest { jti, err := randutil.ASCII(32) assert.FatalError(t, err) cl := jwt.Claims{ @@ -159,7 +159,7 @@ ZEp7knvU2psWRw== Issuer: "step-cli", NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: []string{"step-certificate-authority"}, + Audience: validAud, ID: jti, } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() @@ -178,7 +178,7 @@ ZEp7knvU2psWRw== errMsg: "Unauthorized", } }, - "success": func(t *testing.T) *signTest { + "ok": func(t *testing.T) *signTest { jti, err := randutil.ASCII(32) assert.FatalError(t, err) cl := jwt.Claims{ @@ -186,7 +186,7 @@ ZEp7knvU2psWRw== Issuer: "step-cli", NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: []string{"step-certificate-authority"}, + Audience: validAud, ID: jti, } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() @@ -304,7 +304,11 @@ func TestCAProvisioners(t *testing.T) { var resp api.ProvisionersResponse assert.FatalError(t, readJSON(body, &resp)) - assert.Equals(t, config.AuthorityConfig.Provisioners, resp.Provisioners) + a, err := json.Marshal(config.AuthorityConfig.Provisioners) + assert.FatalError(t, err) + b, err := json.Marshal(resp.Provisioners) + assert.FatalError(t, err) + assert.Equals(t, a, b) } else { err := readError(body) if len(tc.errMsg) == 0 { @@ -597,7 +601,7 @@ func TestCARenew(t *testing.T) { "success": func(t *testing.T) *renewTest { profile, err := x509util.NewLeafProfile("test", intermediateIdentity.Crt, intermediateIdentity.Key, x509util.WithPublicKey(pub), - x509util.WithNotBeforeAfter(now, leafExpiry), x509util.WithHosts("funk")) + x509util.WithNotBeforeAfterDuration(now, leafExpiry, 0), x509util.WithHosts("funk")) assert.FatalError(t, err) crtBytes, err := profile.CreateCertificate() assert.FatalError(t, err) diff --git a/ca/client_test.go b/ca/client_test.go index 0d6f555d..0e231b81 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -13,7 +13,7 @@ import ( "time" "github.com/smallstep/ca-component/api" - "github.com/smallstep/ca-component/provisioner" + "github.com/smallstep/ca-component/authority" ) const ( @@ -390,7 +390,7 @@ func TestClient_Renew(t *testing.T) { func TestClient_Provisioners(t *testing.T) { ok := &api.ProvisionersResponse{ - Provisioners: []*provisioner.Provisioner{}, + Provisioners: []*authority.Provisioner{}, } internalServerError := api.InternalServerError(fmt.Errorf("Internal Server Error")) diff --git a/ca/testdata/ca.json b/ca/testdata/ca.json index 2ddb49b9..b8488b53 100644 --- a/ca/testdata/ca.json +++ b/ca/testdata/ca.json @@ -70,6 +70,9 @@ "alg": "ES256", "x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y", "y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA" + }, + "claims": { + "minTLSCertDuration": "30s" } }, { "issuer": "mariano", diff --git a/ca/tls_test.go b/ca/tls_test.go index 901374b5..89113af2 100644 --- a/ca/tls_test.go +++ b/ca/tls_test.go @@ -45,7 +45,7 @@ func generateOTT(subject string) string { Issuer: "mariano", NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: []string{"step-certificate-authority"}, + Audience: []string{"https://127.0.0.1:0/sign"}, } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() if err != nil { diff --git a/provisioner/provisioner.go b/provisioner/provisioner.go deleted file mode 100644 index 594da019..00000000 --- a/provisioner/provisioner.go +++ /dev/null @@ -1,31 +0,0 @@ -package provisioner - -import ( - "errors" - - jose "gopkg.in/square/go-jose.v2" -) - -// Provisioner - authorized entity that can sign tokens necessary for signature requests. -type Provisioner struct { - Issuer string `json:"issuer,omitempty"` - Type string `json:"type,omitempty"` - Key *jose.JSONWebKey `json:"key,omitempty"` - EncryptedKey string `json:"encryptedKey,omitempty"` -} - -// Validate validates a provisioner. -func (p *Provisioner) Validate() error { - switch { - case p.Issuer == "": - return errors.New("provisioner issuer cannot be empty") - - case p.Type == "": - return errors.New("provisioner type cannot be empty") - - case p.Key == nil: - return errors.New("provisioner key cannot be empty") - } - - return nil -}