change sign + authorize authority api | add provisioners

* authorize returns []interface{}
 - operators in this list can conform to any interface the user decides
 - our implementation has a combination of certificate claim validators
 and certificate template modifiers.
* provisioners can set and enforce tls cert options
This commit is contained in:
max furman 2018-10-18 22:26:39 -07:00
parent d7c31c3133
commit ee7db4006a
20 changed files with 620 additions and 430 deletions

View file

@ -11,43 +11,19 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/pkg/errors" "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/tlsutil"
"github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose" "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. // Authority is the interface implemented by a CA authority.
type Authority interface { type Authority interface {
Authorize(ott string) ([]Claim, error) Authorize(ott string) ([]interface{}, error)
GetTLSOptions() *tlsutil.TLSOptions GetTLSOptions() *tlsutil.TLSOptions
GetMinDuration() time.Duration
GetMaxDuration() time.Duration
Root(shasum string) (*x509.Certificate, error) 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) Renew(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
GetProvisioners() ([]*provisioner.Provisioner, error) GetProvisioners() ([]*authority.Provisioner, error)
GetEncryptedKey(kid string) (string, error) GetEncryptedKey(kid string) (string, error)
} }
@ -176,7 +152,7 @@ type SignRequest struct {
// ProvisionersResponse is the response object that returns the list of // ProvisionersResponse is the response object that returns the list of
// provisioners. // provisioners.
type ProvisionersResponse struct { type ProvisionersResponse struct {
Provisioners []*provisioner.Provisioner `json:"provisioners"` Provisioners []*authority.Provisioner `json:"provisioners"`
} }
// JWKSetByIssuerResponse is the response object that returns the map of // 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")) 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 return nil
} }
@ -243,8 +198,6 @@ type caHandler struct {
// New creates a new RouterHandler with the CA endpoints. // New creates a new RouterHandler with the CA endpoints.
func New(authority Authority) RouterHandler { func New(authority Authority) RouterHandler {
minCertDuration = authority.GetMinDuration()
maxCertDuration = authority.GetMaxDuration()
return &caHandler{ return &caHandler{
Authority: authority, Authority: authority,
} }
@ -296,18 +249,18 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) {
return 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 { if err != nil {
WriteError(w, Unauthorized(err)) WriteError(w, Unauthorized(err))
return return
} }
opts := SignOptions{ cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, signOpts, extraOpts...)
NotBefore: body.NotBefore,
NotAfter: body.NotAfter,
}
cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, claims...)
if err != nil { if err != nil {
WriteError(w, Forbidden(err)) WriteError(w, Forbidden(err))
return return

View file

@ -17,7 +17,7 @@ import (
"time" "time"
"github.com/go-chi/chi" "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/crypto/tlsutil"
"github.com/smallstep/cli/jose" "github.com/smallstep/cli/jose"
) )
@ -349,7 +349,6 @@ func TestCertificateRequest_UnmarshalJSON_json(t *testing.T) {
} }
func TestSignRequest_Validate(t *testing.T) { func TestSignRequest_Validate(t *testing.T) {
now := time.Now()
csr := parseCertificateRequest(csrPEM) csr := parseCertificateRequest(csrPEM)
bad := parseCertificateRequest(csrPEM) bad := parseCertificateRequest(csrPEM)
bad.Signature[0]++ bad.Signature[0]++
@ -364,16 +363,9 @@ func TestSignRequest_Validate(t *testing.T) {
fields fields fields fields
wantErr bool 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}, {"missing csr", fields{CertificateRequest{}, "foobarzar", time.Time{}, time.Time{}}, true},
{"invalid csr", fields{CertificateRequest{bad}, "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}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -393,20 +385,20 @@ func TestSignRequest_Validate(t *testing.T) {
type mockAuthority struct { type mockAuthority struct {
ret1, ret2 interface{} ret1, ret2 interface{}
err error err error
authorize func(ott string) ([]Claim, error) authorize func(ott string) ([]interface{}, error)
getTLSOptions func() *tlsutil.TLSOptions getTLSOptions func() *tlsutil.TLSOptions
root func(shasum string) (*x509.Certificate, error) 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) 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) 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 { if m.authorize != nil {
return m.authorize(ott) return m.authorize(ott)
} }
return m.ret1.([]Claim), m.err return m.ret1.([]interface{}), m.err
} }
func (m *mockAuthority) GetTLSOptions() *tlsutil.TLSOptions { func (m *mockAuthority) GetTLSOptions() *tlsutil.TLSOptions {
@ -416,14 +408,6 @@ func (m *mockAuthority) GetTLSOptions() *tlsutil.TLSOptions {
return m.ret1.(*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) { func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) {
if m.root != nil { if m.root != nil {
return m.root(shasum) return m.root(shasum)
@ -431,9 +415,9 @@ func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) {
return m.ret1.(*x509.Certificate), m.err 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 { 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 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 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 { if m.getProvisioners != nil {
return m.getProvisioners() 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) { func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) {
@ -569,7 +553,7 @@ func Test_caHandler_Sign(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
claims []Claim certAttrOpts []interface{}
autherr error autherr error
cert *x509.Certificate cert *x509.Certificate
root *x509.Certificate root *x509.Certificate
@ -589,8 +573,8 @@ func Test_caHandler_Sign(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
h := New(&mockAuthority{ h := New(&mockAuthority{
ret1: tt.cert, ret2: tt.root, err: tt.signErr, ret1: tt.cert, ret2: tt.root, err: tt.signErr,
authorize: func(ott string) ([]Claim, error) { authorize: func(ott string) ([]interface{}, error) {
return tt.claims, tt.autherr return tt.certAttrOpts, tt.autherr
}, },
getTLSOptions: func() *tlsutil.TLSOptions { getTLSOptions: func() *tlsutil.TLSOptions {
return nil return nil
@ -690,7 +674,7 @@ func Test_caHandler_JWKSetByIssuer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
p := []*provisioner.Provisioner{ p := []*authority.Provisioner{
{ {
Issuer: "p1", Issuer: "p1",
Key: &key, Key: &key,
@ -766,7 +750,7 @@ func Test_caHandler_Provisioners(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
p := []*provisioner.Provisioner{ p := []*authority.Provisioner{
{ {
Type: "JWK", Type: "JWK",
Issuer: "max", Issuer: "max",

View file

@ -7,7 +7,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/ca-component/provisioner"
stepJOSE "github.com/smallstep/cli/jose" stepJOSE "github.com/smallstep/cli/jose"
) )
@ -16,7 +15,7 @@ func testAuthority(t *testing.T) *Authority {
assert.FatalError(t, err) assert.FatalError(t, err)
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk") clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
assert.FatalError(t, err) assert.FatalError(t, err)
p := []*provisioner.Provisioner{ p := []*Provisioner{
{ {
Issuer: "Max", Issuer: "Max",
Type: "JWK", Type: "JWK",
@ -29,11 +28,11 @@ func testAuthority(t *testing.T) *Authority {
}, },
} }
c := &Config{ c := &Config{
Address: "127.0.0.1", Address: "127.0.0.1:443",
Root: "testdata/secrets/root_ca.crt", Root: "testdata/secrets/root_ca.crt",
IntermediateCert: "testdata/secrets/intermediate_ca.crt", IntermediateCert: "testdata/secrets/intermediate_ca.crt",
IntermediateKey: "testdata/secrets/intermediate_ca_key", IntermediateKey: "testdata/secrets/intermediate_ca_key",
DNSNames: []string{"test.smallstep.com"}, DNSNames: []string{"test.ca.smallstep.com"},
Password: "pass", Password: "pass",
AuthorityConfig: &AuthConfig{ AuthorityConfig: &AuthConfig{
Provisioners: p, Provisioners: p,

View file

@ -5,8 +5,6 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/ca-component/api"
"github.com/smallstep/ca-component/provisioner"
"gopkg.in/square/go-jose.v2/jwt" "gopkg.in/square/go-jose.v2/jwt"
) )
@ -15,17 +13,19 @@ type idUsed struct {
Subject string `json:"sub,omitempty"` Subject string `json:"sub,omitempty"`
} }
func containsAtLeastOneAudience(claim []string, expected []string) bool { // containsAtLeastOneAudience returns true if 'as' contains at least one element
if len(expected) == 0 { // of 'bs', otherwise returns false.
func containsAtLeastOneAudience(as []string, bs []string) bool {
if len(bs) == 0 {
return true return true
} }
if len(claim) == 0 { if len(as) == 0 {
return false return false
} }
for _, exp := range expected { for _, b := range bs {
for _, cl := range claim { for _, a := range as {
if exp == cl { if b == a {
return true return true
} }
} }
@ -35,35 +35,33 @@ func containsAtLeastOneAudience(claim []string, expected []string) bool {
// Authorize authorizes a signature request by validating and authenticating // Authorize authorizes a signature request by validating and authenticating
// a OTT that must be sent w/ the request. // 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 ( var (
errContext = map[string]interface{}{"ott": ott} errContext = map[string]interface{}{"ott": ott}
claims = jwt.Claims{} claims = jwt.Claims{}
// Claims to check in the Sign method
downstreamClaims []api.Claim
) )
// Validate payload // Validate payload
token, err := jwt.ParseSigned(ott) token, err := jwt.ParseSigned(ott)
if err != nil { 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} http.StatusUnauthorized, errContext}
} }
kid := token.Headers[0].KeyID // JWT will only have 1 header. kid := token.Headers[0].KeyID // JWT will only have 1 header.
if len(kid) == 0 { 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} http.StatusUnauthorized, errContext}
} }
val, ok := a.provisionerIDIndex.Load(kid) val, ok := a.provisionerIDIndex.Load(kid)
if !ok { 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} http.StatusUnauthorized, errContext}
} }
p, ok := val.(*provisioner.Provisioner) p, ok := val.(*Provisioner)
if !ok { if !ok {
return nil, &apiError{errors.Errorf("stored value is not a *Provisioner"), return nil, &apiError{errors.Errorf("authorize: invalid provisioner type"),
http.StatusInternalServerError, context{}} http.StatusInternalServerError, errContext}
} }
if err = token.Claims(p.Key, &claims); err != nil { 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{ if err = claims.ValidateWithLeeway(jwt.Expected{
Issuer: p.Issuer, Issuer: p.Issuer,
}, time.Minute); err != nil { }, 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} http.StatusUnauthorized, errContext}
} }
@ -89,17 +87,22 @@ func (a *Authority) Authorize(ott string) ([]api.Claim, error) {
} }
if !containsAtLeastOneAudience(claims.Audience, a.audiences) { 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} errContext}
} }
if claims.Subject == "" { 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} http.StatusUnauthorized, errContext}
} }
downstreamClaims = append(downstreamClaims, &commonNameClaim{claims.Subject})
downstreamClaims = append(downstreamClaims, &dnsNamesClaim{claims.Subject}) signOps := []interface{}{
downstreamClaims = append(downstreamClaims, &ipAddressesClaim{claims.Subject}) &commonNameClaim{claims.Subject},
&dnsNamesClaim{claims.Subject},
&ipAddressesClaim{claims.Subject},
withIssuerAlternativeNameExtension(p.Issuer + ":" + p.Key.KeyID),
p,
}
// Store the token to protect against reuse. // Store the token to protect against reuse.
if _, ok := a.ottMap.LoadOrStore(claims.ID, &idUsed{ if _, ok := a.ottMap.LoadOrStore(claims.ID, &idUsed{
@ -110,5 +113,5 @@ func (a *Authority) Authorize(ott string) ([]api.Claim, error) {
errContext} errContext}
} }
return downstreamClaims, nil return signOps, nil
} }

View file

@ -7,7 +7,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/ca-component/api"
"github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/crypto/keys"
stepJOSE "github.com/smallstep/cli/jose" stepJOSE "github.com/smallstep/cli/jose"
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
@ -27,53 +26,127 @@ func TestAuthorize(t *testing.T) {
now := time.Now() now := time.Now()
validIssuer := "step-cli" validIssuer := "step-cli"
validAudience := []string{"https://test.ca.smallstep.com/sign"}
type authorizeTest struct { type authorizeTest struct {
auth *Authority
ott string ott string
err *apiError err *apiError
claims []api.Claim res []interface{}
} }
tests := map[string]func(t *testing.T) *authorizeTest{ 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{ return &authorizeTest{
auth: a,
ott: "foo", ott: "foo",
err: &apiError{errors.New("error parsing OTT"), err: &apiError{errors.New("authorize: error parsing token"),
http.StatusUnauthorized, context{"ott": "foo"}}, 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{ cl := jwt.Claims{
Subject: "subject", Subject: "subject",
Issuer: "invalid-issuer", Issuer: "invalid-issuer",
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validTokenAudience, Audience: validAudience,
} }
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err) assert.FatalError(t, err)
return &authorizeTest{ return &authorizeTest{
auth: a,
ott: raw, ott: raw,
err: &apiError{errors.New("error validating OTT"), err: &apiError{errors.New("authorize: invalid token"),
http.StatusUnauthorized, context{"ott": raw}}, 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{ cl := jwt.Claims{
Subject: "", Subject: "",
Issuer: validIssuer, Issuer: validIssuer,
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validTokenAudience, Audience: validAudience,
} }
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err) assert.FatalError(t, err)
return &authorizeTest{ return &authorizeTest{
auth: a,
ott: raw, ott: raw,
err: &apiError{errors.New("OTT sub cannot be empty"), err: &apiError{errors.New("authorize: token subject cannot be empty"),
http.StatusUnauthorized, context{"ott": raw}}, 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() _, priv2, err := keys.GenerateDefaultKeyPair()
assert.FatalError(t, err) assert.FatalError(t, err)
invalidKeySig, err := jose.NewSigner(jose.SigningKey{ invalidKeySig, err := jose.NewSigner(jose.SigningKey{
@ -82,27 +155,28 @@ func TestAuthorize(t *testing.T) {
}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) }, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
assert.FatalError(t, err) assert.FatalError(t, err)
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "foo", Subject: "test.smallstep.com",
Issuer: validIssuer, Issuer: validIssuer,
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validTokenAudience, Audience: validAudience,
} }
raw, err := jwt.Signed(invalidKeySig).Claims(cl).CompactSerialize() raw, err := jwt.Signed(invalidKeySig).Claims(cl).CompactSerialize()
assert.FatalError(t, err) assert.FatalError(t, err)
return &authorizeTest{ return &authorizeTest{
auth: a,
ott: raw, ott: raw,
err: &apiError{errors.New("square/go-jose: error in cryptographic primitive"), err: &apiError{errors.New("square/go-jose: error in cryptographic primitive"),
http.StatusUnauthorized, context{"ott": raw}}, 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{ cl := jwt.Claims{
Subject: "foo", Subject: "test.smallstep.com",
Issuer: validIssuer, Issuer: validIssuer,
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validTokenAudience, Audience: validAudience,
ID: "42", ID: "42",
} }
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
@ -110,25 +184,31 @@ func TestAuthorize(t *testing.T) {
_, err = a.Authorize(raw) _, err = a.Authorize(raw)
assert.FatalError(t, err) assert.FatalError(t, err)
return &authorizeTest{ return &authorizeTest{
auth: a,
ott: raw, ott: raw,
err: &apiError{errors.New("token already used"), err: &apiError{errors.New("token already used"),
http.StatusUnauthorized, context{"ott": raw}}, http.StatusUnauthorized, context{"ott": raw}},
claims: nil} }
}, },
"success": func(t *testing.T) *authorizeTest { "ok": func(t *testing.T) *authorizeTest {
cl := jwt.Claims{ cl := jwt.Claims{
Subject: "foo", Subject: "test.smallstep.com",
Issuer: validIssuer, Issuer: validIssuer,
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: validTokenAudience, Audience: validAudience,
ID: "43", ID: "43",
} }
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err) assert.FatalError(t, err)
return &authorizeTest{ return &authorizeTest{
auth: a,
ott: raw, ott: raw,
claims: []api.Claim{&commonNameClaim{"foo"}, &dnsNamesClaim{"foo"}, &ipAddressesClaim{"foo"}}, res: []interface{}{
"1", "2", "3",
withIssuerAlternativeNameExtension("step-cli:" + jwk.KeyID),
"5",
},
} }
}, },
} }
@ -138,7 +218,7 @@ func TestAuthorize(t *testing.T) {
tc := genTestCase(t) tc := genTestCase(t)
assert.FatalError(t, err) assert.FatalError(t, err)
claims, err := a.Authorize(tc.ott) crtOpts, err := tc.auth.Authorize(tc.ott)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
switch v := err.(type) { switch v := err.(type) {
@ -152,7 +232,7 @@ func TestAuthorize(t *testing.T) {
} }
} else { } else {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
assert.Equals(t, claims, tc.claims) assert.Equals(t, len(crtOpts), len(tc.res))
} }
} }
}) })

View file

@ -1,18 +1,24 @@
package authority package authority
import ( import (
"crypto/x509"
"net" "net"
"time"
"github.com/pkg/errors" "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 // ValidateClaims returns nil if all the claims are validated, it will return
// the first error if a claim fails. // 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 { for _, c := range claims {
if err = c.Valid(cr); err != nil { if err = c.Valid(crt); err != nil {
return err return err
} }
} }
@ -25,12 +31,12 @@ type commonNameClaim struct {
} }
// Valid checks that certificate request common name matches the one configured. // Valid checks that certificate request common name matches the one configured.
func (c *commonNameClaim) Valid(cr *x509.CertificateRequest) error { func (c *commonNameClaim) Valid(crt *x509.Certificate) error {
if cr.Subject.CommonName == "" { if crt.Subject.CommonName == "" {
return errors.New("common name cannot be empty") return errors.New("common name cannot be empty")
} }
if cr.Subject.CommonName != c.name { if crt.Subject.CommonName != c.name {
return errors.Errorf("common name claim failed - got %s, want %s", cr.Subject.CommonName, c.name) return errors.Errorf("common name claim failed - got %s, want %s", crt.Subject.CommonName, c.name)
} }
return nil return nil
} }
@ -40,11 +46,11 @@ type dnsNamesClaim struct {
} }
// Valid checks that certificate request common name matches the one configured. // Valid checks that certificate request common name matches the one configured.
func (c *dnsNamesClaim) Valid(cr *x509.CertificateRequest) error { func (c *dnsNamesClaim) Valid(crt *x509.Certificate) error {
if len(cr.DNSNames) == 0 { if len(crt.DNSNames) == 0 {
return nil return nil
} }
for _, name := range cr.DNSNames { for _, name := range crt.DNSNames {
if name != c.name { if name != c.name {
return errors.Errorf("DNS names claim failed - got %s, want %s", 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. // Valid checks that certificate request common name matches the one configured.
func (c *ipAddressesClaim) Valid(cr *x509.CertificateRequest) error { func (c *ipAddressesClaim) Valid(crt *x509.Certificate) error {
if len(cr.IPAddresses) == 0 { if len(crt.IPAddresses) == 0 {
return nil return nil
} }
// If it's an IP validate that only that ip is in IP addresses // If it's an IP validate that only that ip is in IP addresses
if requestedIP := net.ParseIP(c.name); requestedIP != nil { if requestedIP := net.ParseIP(c.name); requestedIP != nil {
for _, ip := range cr.IPAddresses { for _, ip := range crt.IPAddresses {
if !ip.Equal(requestedIP) { if !ip.Equal(requestedIP) {
return errors.Errorf("IP addresses claim failed - got %s, want %s", ip, 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 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
} }

View file

@ -1,35 +1,34 @@
package authority package authority
import ( import (
"crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"net" "net"
"testing" "testing"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/ca-component/api" x509 "github.com/smallstep/cli/pkg/x509"
) )
func TestCommonNameClaim_Valid(t *testing.T) { func TestCommonNameClaim_Valid(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
cnc api.Claim cnc certClaim
crt *x509.CertificateRequest crt *x509.Certificate
err error err error
}{ }{
"empty-common-name": { "empty-common-name": {
cnc: &commonNameClaim{name: "foo"}, cnc: &commonNameClaim{name: "foo"},
crt: &x509.CertificateRequest{}, crt: &x509.Certificate{},
err: errors.New("common name cannot be empty"), err: errors.New("common name cannot be empty"),
}, },
"wrong-common-name": { "wrong-common-name": {
cnc: &commonNameClaim{name: "foo"}, 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"), err: errors.New("common name claim failed - got bar, want foo"),
}, },
"ok": { "ok": {
cnc: &commonNameClaim{name: "foo"}, 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) { func TestIPAddressesClaim_Valid(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
iac api.Claim iac certClaim
crt *x509.CertificateRequest crt *x509.Certificate
err error err error
}{ }{
"unexpected-ip": { "unexpected-ip": {
iac: &ipAddressesClaim{name: "127.0.0.1"}, 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"), err: errors.New("IP addresses claim failed - got 1.1.1.1, want 127.0.0.1"),
}, },
"invalid-matcher-nonempty-ips": { "invalid-matcher-nonempty-ips": {
iac: &ipAddressesClaim{name: "invalid"}, 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"), err: errors.New("IP addresses claim failed - got [127.0.0.1], want none"),
}, },
"ok": { "ok": {
iac: &ipAddressesClaim{name: "127.0.0.1"}, 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": { "ok-empty-ips": {
iac: &ipAddressesClaim{name: "127.0.0.1"}, 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) { func TestDNSNamesClaim_Valid(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
dnc api.Claim dnc certClaim
crt *x509.CertificateRequest crt *x509.Certificate
err error err error
}{ }{
"wrong-dns-name": { "wrong-dns-name": {
dnc: &dnsNamesClaim{name: "foo"}, 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"), err: errors.New("DNS names claim failed - got bar, want foo"),
}, },
"ok": { "ok": {
dnc: &dnsNamesClaim{name: "foo"}, dnc: &dnsNamesClaim{name: "foo"},
crt: &x509.CertificateRequest{DNSNames: []string{"foo"}}, crt: &x509.Certificate{DNSNames: []string{"foo"}},
}, },
"ok-empty-dnsNames": { "ok-empty-dnsNames": {
dnc: &dnsNamesClaim{"foo"}, dnc: &dnsNamesClaim{"foo"},
crt: &x509.CertificateRequest{}, crt: &x509.Certificate{},
}, },
"ok-multiple-identical-dns-entries": { "ok-multiple-identical-dns-entries": {
dnc: &dnsNamesClaim{name: "foo"}, dnc: &dnsNamesClaim{name: "foo"},
crt: &x509.CertificateRequest{DNSNames: []string{"foo", "foo", "foo"}}, crt: &x509.Certificate{DNSNames: []string{"foo", "foo", "foo"}},
}, },
} }

View file

@ -6,14 +6,14 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/ca-component/provisioner"
"github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/crypto/x509util"
) )
var (
// DefaultTLSOptions represents the default TLS version as well as the cipher // DefaultTLSOptions represents the default TLS version as well as the cipher
// suites used in the TLS certificates. // suites used in the TLS certificates.
var DefaultTLSOptions = tlsutil.TLSOptions{ DefaultTLSOptions = tlsutil.TLSOptions{
CipherSuites: x509util.CipherSuites{ CipherSuites: x509util.CipherSuites{
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
@ -23,18 +23,26 @@ var DefaultTLSOptions = tlsutil.TLSOptions{
MaxVersion: 1.2, MaxVersion: 1.2,
Renegotiation: false, Renegotiation: false,
} }
globalProvisionerClaims = ProvisionerClaims{
const ( MinTLSDur: &duration{5 * time.Minute},
// minCertDuration is the minimum validity of an end-entity (not root or intermediate) certificate. MaxTLSDur: &duration{24 * time.Hour},
minCertDuration = 5 * time.Minute DefaultTLSDur: &duration{24 * time.Hour},
// maxCertDuration is the maximum validity of an end-entity (not root or intermediate) certificate. }
maxCertDuration = 24 * time.Hour
) )
type duration struct { type duration struct {
time.Duration 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. // UnmarshalJSON parses a duration string and sets it to the duration.
// //
// A duration string is a possibly signed sequence of decimal numbers, each with // 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. // AuthConfig represents the configuration options for the authority.
type AuthConfig struct { type AuthConfig struct {
Provisioners []*provisioner.Provisioner `json:"provisioners,omitempty"` Provisioners []*Provisioner `json:"provisioners,omitempty"`
Template *x509util.ASN1DN `json:"template,omitempty"` Template *x509util.ASN1DN `json:"template,omitempty"`
MinCertDuration *duration `json:"minCertDuration,omitempty"` Claims *ProvisionerClaims
MaxCertDuration *duration `json:"maxCertDuration,omitempty"`
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
} }
// Validate validates the authority configuration. // Validate validates the authority configuration.
func (c *AuthConfig) Validate() error { func (c *AuthConfig) Validate() error {
var err error
if c == nil { if c == nil {
return errors.New("authority cannot be undefined") return errors.New("authority cannot be undefined")
} }
if len(c.Provisioners) == 0 { if len(c.Provisioners) == 0 {
return errors.New("authority.provisioners cannot be empty") 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 { for _, p := range c.Provisioners {
err := p.Validate() if err := p.Init(c.Claims); err != nil {
if err != nil {
return err return err
} }
} }

View file

@ -5,7 +5,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/ca-component/provisioner"
"github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/crypto/x509util"
stepJOSE "github.com/smallstep/cli/jose" 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") clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
assert.FatalError(t, err) assert.FatalError(t, err)
ac := &AuthConfig{ ac := &AuthConfig{
Provisioners: []*provisioner.Provisioner{ Provisioners: []*Provisioner{
{ {
Issuer: "Max", Issuer: "Max",
Type: "JWK", Type: "JWK",
@ -216,7 +215,7 @@ func TestAuthConfigValidate(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk") clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
assert.FatalError(t, err) assert.FatalError(t, err)
p := []*provisioner.Provisioner{ p := []*Provisioner{
{ {
Issuer: "Max", Issuer: "Max",
Type: "JWK", Type: "JWK",
@ -250,7 +249,7 @@ func TestAuthConfigValidate(t *testing.T) {
"fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest { "fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest {
return AuthConfigValidateTest{ return AuthConfigValidateTest{
ac: &AuthConfig{ ac: &AuthConfig{
Provisioners: []*provisioner.Provisioner{ Provisioners: []*Provisioner{
{Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}}, {Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}},
{Issuer: "foo", Key: &jose.JSONWebKey{}}, {Issuer: "foo", Key: &jose.JSONWebKey{}},
}, },

133
authority/provisioner.go Normal file
View file

@ -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
}

View file

@ -1,4 +1,4 @@
package provisioner package authority
import ( import (
"errors" "errors"
@ -8,7 +8,7 @@ import (
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
) )
func TestProvisionerValidate(t *testing.T) { func TestProvisionerInit(t *testing.T) {
type ProvisionerValidateTest struct { type ProvisionerValidateTest struct {
p *Provisioner p *Provisioner
err error err error
@ -42,7 +42,7 @@ func TestProvisionerValidate(t *testing.T) {
for name, get := range tests { for name, get := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
tc := get(t) tc := get(t)
err := tc.p.Validate() err := tc.p.Init(&globalProvisionerClaims)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.Equals(t, tc.err.Error(), err.Error()) assert.Equals(t, tc.err.Error(), err.Error())

View file

@ -4,7 +4,6 @@ import (
"net/http" "net/http"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/ca-component/provisioner"
) )
// GetEncryptedKey returns the JWE key corresponding to the given kid argument. // 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 // GetProvisioners returns a map listing each provisioner and the JWK Key Set
// with their public keys. // with their public keys.
func (a *Authority) GetProvisioners() ([]*provisioner.Provisioner, error) { func (a *Authority) GetProvisioners() ([]*Provisioner, error) {
return a.config.AuthorityConfig.Provisioners, nil return a.config.AuthorityConfig.Provisioners, nil
} }

View file

@ -6,7 +6,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/ca-component/provisioner"
) )
func TestGetEncryptedKey(t *testing.T) { func TestGetEncryptedKey(t *testing.T) {
@ -73,7 +72,7 @@ func TestGetEncryptedKey(t *testing.T) {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
val, ok := tc.a.provisionerIDIndex.Load(tc.kid) val, ok := tc.a.provisionerIDIndex.Load(tc.kid)
assert.Fatal(t, ok) assert.Fatal(t, ok)
p, ok := val.(*provisioner.Provisioner) p, ok := val.(*Provisioner)
assert.Fatal(t, ok) assert.Fatal(t, ok)
assert.Equals(t, p.EncryptedKey, ek) assert.Equals(t, p.EncryptedKey, ek)
} }

View file

@ -3,42 +3,58 @@ package authority
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem" "encoding/pem"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/ca-component/api"
"github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/crypto/x509util"
stepx509 "github.com/smallstep/cli/pkg/x509" 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. // GetTLSOptions returns the tls options configured.
func (a *Authority) GetTLSOptions() *tlsutil.TLSOptions { func (a *Authority) GetTLSOptions() *tlsutil.TLSOptions {
return a.config.TLS 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 { func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
return func(p x509util.Profile) error { return func(p x509util.Profile) error {
if def == nil { if def == nil {
@ -70,17 +86,40 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
} }
// Sign creates a signed certificate from a certificate signing request. // 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) { func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error) {
if err := ValidateClaims(csr, claims); err != nil { var (
return nil, nil, &apiError{err, http.StatusUnauthorized, context{}} 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) stepCSR, err := stepx509.ParseCertificateRequest(csr.Raw)
if err != nil { if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "error converting x509 csr to stepx509 csr"), return nil, nil, &apiError{errors.Wrap(err, "sign: error converting x509 csr to stepx509 csr"),
http.StatusInternalServerError, context{}} http.StatusInternalServerError, errContext}
} }
// DNSNames and IPAddresses are validated but to avoid duplications we will // DNSNames and IPAddresses are validated but to avoid duplications we will
// clean them as x509util.NewLeafProfileWithCSR will set the right values. // clean them as x509util.NewLeafProfileWithCSR will set the right values.
stepCSR.DNSNames = nil stepCSR.DNSNames = nil
@ -88,27 +127,31 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, opts api.SignOptions, cla
issIdentity := a.intermediateIdentity issIdentity := a.intermediateIdentity
leaf, err := x509util.NewLeafProfileWithCSR(stepCSR, issIdentity.Crt, leaf, err := x509util.NewLeafProfileWithCSR(stepCSR, issIdentity.Crt,
issIdentity.Key, x509util.WithHosts(csr.Subject.CommonName), issIdentity.Key, mods...)
x509util.WithNotBeforeAfter(opts.NotBefore, opts.NotAfter),
withDefaultASN1DN(a.config.AuthorityConfig.Template))
if err != nil { 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() crtBytes, err := leaf.CreateCertificate()
if err != nil { if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "error creating new leaf certificate from input csr"), return nil, nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"),
http.StatusInternalServerError, context{}} http.StatusInternalServerError, errContext}
} }
serverCert, err := x509.ParseCertificate(crtBytes) serverCert, err := x509.ParseCertificate(crtBytes)
if err != nil { if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "error parsing new server certificate"), return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"),
http.StatusInternalServerError, context{}} http.StatusInternalServerError, errContext}
} }
caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw)
if err != nil { if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "error parsing intermediate certificate"), return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"),
http.StatusInternalServerError, context{}} http.StatusInternalServerError, errContext}
} }
return serverCert, caCert, nil return serverCert, caCert, nil

View file

@ -12,7 +12,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/ca-component/api"
"github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/crypto/keys"
"github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/crypto/x509util"
@ -20,7 +19,7 @@ import (
func getCSR(t *testing.T, priv interface{}) *x509.CertificateRequest { func getCSR(t *testing.T, priv interface{}) *x509.CertificateRequest {
_csr := &x509.CertificateRequest{ _csr := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: "test"}, Subject: pkix.Name{CommonName: "test.smallstep.com"},
DNSNames: []string{"test.smallstep.com"}, DNSNames: []string{"test.smallstep.com"},
} }
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv) csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)
@ -42,90 +41,121 @@ func TestSign(t *testing.T) {
Locality: "Landscapes", Locality: "Landscapes",
Province: "Sudden Cliffs", Province: "Sudden Cliffs",
StreetAddress: "TNT", 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 { type signTest struct {
auth *Authority auth *Authority
csr *x509.CertificateRequest csr *x509.CertificateRequest
opts api.SignOptions signOpts SignOptions
claims []api.Claim extraOpts []interface{}
err *apiError err *apiError
} }
tests := map[string]func(*testing.T) *signTest{ tests := map[string]func(*testing.T) *signTest{
"fail-validate-claims": func(t *testing.T) *signTest { "fail invalid extra option": 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 {
csr := getCSR(t, priv) csr := getCSR(t, priv)
csr.Raw = []byte("foo") csr.Raw = []byte("foo")
return &signTest{ return &signTest{
auth: a, auth: a,
csr: csr, csr: csr,
opts: api.SignOptions{ extraOpts: []interface{}{
NotBefore: now, withIssuerAlternativeNameExtension("baz"),
NotAfter: now.Add(time.Minute * 5), "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 := testAuthority(t)
_a.config.AuthorityConfig.Template = nil _a.config.AuthorityConfig.Template = nil
csr := getCSR(t, priv) csr := getCSR(t, priv)
return &signTest{ return &signTest{
auth: _a, auth: _a,
csr: csr, csr: csr,
opts: api.SignOptions{ extraOpts: []interface{}{
NotBefore: now, withIssuerAlternativeNameExtension("baz"),
NotAfter: now.Add(time.Minute * 5), 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 := testAuthority(t)
_a.intermediateIdentity.Key = nil _a.intermediateIdentity.Key = nil
csr := getCSR(t, priv) csr := getCSR(t, priv)
return &signTest{ return &signTest{
auth: _a, auth: _a,
csr: csr, csr: csr,
opts: api.SignOptions{ extraOpts: []interface{}{
NotBefore: now, withIssuerAlternativeNameExtension("baz"),
NotAfter: now.Add(time.Minute * 5), },
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) csr := getCSR(t, priv)
return &signTest{ return &signTest{
auth: a, auth: a,
csr: csr, csr: csr,
opts: api.SignOptions{ extraOpts: []interface{}{
NotBefore: now, withIssuerAlternativeNameExtension("baz"),
NotAfter: now.Add(time.Minute * 5), 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) { t.Run(name, func(t *testing.T) {
tc := genTestCase(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 err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
switch v := err.(type) { switch v := err.(type) {
@ -148,8 +178,8 @@ func TestSign(t *testing.T) {
} }
} else { } else {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
assert.Equals(t, leaf.NotBefore, tc.opts.NotBefore.UTC().Truncate(time.Second)) assert.Equals(t, leaf.NotBefore, signOpts.NotBefore.UTC().Truncate(time.Second))
assert.Equals(t, leaf.NotAfter, tc.opts.NotAfter.UTC().Truncate(time.Second)) assert.Equals(t, leaf.NotAfter, signOpts.NotAfter.UTC().Truncate(time.Second))
tmplt := a.config.AuthorityConfig.Template tmplt := a.config.AuthorityConfig.Template
assert.Equals(t, fmt.Sprintf("%v", leaf.Subject), assert.Equals(t, fmt.Sprintf("%v", leaf.Subject),
fmt.Sprintf("%v", &pkix.Name{ 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.PublicKeyAlgorithm, x509.ECDSA)
assert.Equals(t, leaf.ExtKeyUsage, assert.Equals(t, leaf.ExtKeyUsage,
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) []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) pubBytes, err := x509.MarshalPKIXPublicKey(pub)
assert.FatalError(t, err) assert.FatalError(t, err)
@ -201,14 +231,14 @@ func TestRenew(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
nb1 := now.Add(-time.Minute * 7) nb1 := now.Add(-time.Minute * 7)
na1 := now na1 := now
so := &api.SignOptions{ so := &SignOptions{
NotBefore: nb1, NotBefore: nb1,
NotAfter: na1, NotAfter: na1,
} }
leaf, err := x509util.NewLeafProfile("renew", a.intermediateIdentity.Crt, leaf, err := x509util.NewLeafProfile("renew", a.intermediateIdentity.Crt,
a.intermediateIdentity.Key, a.intermediateIdentity.Key,
x509util.WithNotBeforeAfter(so.NotBefore, so.NotAfter), x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, 0),
withDefaultASN1DN(a.config.AuthorityConfig.Template), withDefaultASN1DN(a.config.AuthorityConfig.Template),
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test")) x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"))
assert.FatalError(t, err) 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) { func TestGetTLSOptions(t *testing.T) {
type renewTest struct { type renewTest struct {
auth *Authority auth *Authority

View file

@ -75,10 +75,10 @@ func TestCASign(t *testing.T) {
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_priv.jwk", clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_priv.jwk",
stepJOSE.WithPassword([]byte("pass"))) stepJOSE.WithPassword([]byte("pass")))
assert.FatalError(t, err) assert.FatalError(t, err)
fmt.Printf("clijwk.KeyID = %+v\n", clijwk.KeyID)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: clijwk.Key}, sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: clijwk.Key},
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", clijwk.KeyID)) (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", clijwk.KeyID))
assert.FatalError(t, err) assert.FatalError(t, err)
validAud := []string{"https://127.0.0.1:0/sign"}
now := time.Now().UTC() now := time.Now().UTC()
leafExpiry := now.Add(time.Minute * 5) leafExpiry := now.Add(time.Minute * 5)
@ -90,7 +90,7 @@ func TestCASign(t *testing.T) {
errMsg string errMsg string
} }
tests := map[string]func(t *testing.T) *signTest{ 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{ return &signTest{
ca: ca, ca: ca,
body: "invalid json", body: "invalid json",
@ -98,7 +98,7 @@ func TestCASign(t *testing.T) {
errMsg: "Bad Request", 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----- der := []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH
DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl
@ -136,7 +136,7 @@ ZEp7knvU2psWRw==
errMsg: "Bad Request", errMsg: "Bad Request",
} }
}, },
"unauthorized-ott": func(t *testing.T) *signTest { "fail unauthorized-ott": func(t *testing.T) *signTest {
csr, err := getCSR(priv) csr, err := getCSR(priv)
assert.FatalError(t, err) assert.FatalError(t, err)
body, err := json.Marshal(&api.SignRequest{ body, err := json.Marshal(&api.SignRequest{
@ -151,7 +151,7 @@ ZEp7knvU2psWRw==
errMsg: "Unauthorized", errMsg: "Unauthorized",
} }
}, },
"fail-commonname-claim": func(t *testing.T) *signTest { "fail commonname-claim": func(t *testing.T) *signTest {
jti, err := randutil.ASCII(32) jti, err := randutil.ASCII(32)
assert.FatalError(t, err) assert.FatalError(t, err)
cl := jwt.Claims{ cl := jwt.Claims{
@ -159,7 +159,7 @@ ZEp7knvU2psWRw==
Issuer: "step-cli", Issuer: "step-cli",
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: []string{"step-certificate-authority"}, Audience: validAud,
ID: jti, ID: jti,
} }
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
@ -178,7 +178,7 @@ ZEp7knvU2psWRw==
errMsg: "Unauthorized", errMsg: "Unauthorized",
} }
}, },
"success": func(t *testing.T) *signTest { "ok": func(t *testing.T) *signTest {
jti, err := randutil.ASCII(32) jti, err := randutil.ASCII(32)
assert.FatalError(t, err) assert.FatalError(t, err)
cl := jwt.Claims{ cl := jwt.Claims{
@ -186,7 +186,7 @@ ZEp7knvU2psWRw==
Issuer: "step-cli", Issuer: "step-cli",
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
Audience: []string{"step-certificate-authority"}, Audience: validAud,
ID: jti, ID: jti,
} }
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
@ -304,7 +304,11 @@ func TestCAProvisioners(t *testing.T) {
var resp api.ProvisionersResponse var resp api.ProvisionersResponse
assert.FatalError(t, readJSON(body, &resp)) 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 { } else {
err := readError(body) err := readError(body)
if len(tc.errMsg) == 0 { if len(tc.errMsg) == 0 {
@ -597,7 +601,7 @@ func TestCARenew(t *testing.T) {
"success": func(t *testing.T) *renewTest { "success": func(t *testing.T) *renewTest {
profile, err := x509util.NewLeafProfile("test", intermediateIdentity.Crt, profile, err := x509util.NewLeafProfile("test", intermediateIdentity.Crt,
intermediateIdentity.Key, x509util.WithPublicKey(pub), intermediateIdentity.Key, x509util.WithPublicKey(pub),
x509util.WithNotBeforeAfter(now, leafExpiry), x509util.WithHosts("funk")) x509util.WithNotBeforeAfterDuration(now, leafExpiry, 0), x509util.WithHosts("funk"))
assert.FatalError(t, err) assert.FatalError(t, err)
crtBytes, err := profile.CreateCertificate() crtBytes, err := profile.CreateCertificate()
assert.FatalError(t, err) assert.FatalError(t, err)

View file

@ -13,7 +13,7 @@ import (
"time" "time"
"github.com/smallstep/ca-component/api" "github.com/smallstep/ca-component/api"
"github.com/smallstep/ca-component/provisioner" "github.com/smallstep/ca-component/authority"
) )
const ( const (
@ -390,7 +390,7 @@ func TestClient_Renew(t *testing.T) {
func TestClient_Provisioners(t *testing.T) { func TestClient_Provisioners(t *testing.T) {
ok := &api.ProvisionersResponse{ ok := &api.ProvisionersResponse{
Provisioners: []*provisioner.Provisioner{}, Provisioners: []*authority.Provisioner{},
} }
internalServerError := api.InternalServerError(fmt.Errorf("Internal Server Error")) internalServerError := api.InternalServerError(fmt.Errorf("Internal Server Error"))

3
ca/testdata/ca.json vendored
View file

@ -70,6 +70,9 @@
"alg": "ES256", "alg": "ES256",
"x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y", "x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y",
"y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA" "y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA"
},
"claims": {
"minTLSCertDuration": "30s"
} }
}, { }, {
"issuer": "mariano", "issuer": "mariano",

View file

@ -45,7 +45,7 @@ func generateOTT(subject string) string {
Issuer: "mariano", Issuer: "mariano",
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(time.Minute)), 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() raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
if err != nil { if err != nil {

View file

@ -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
}