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/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

View file

@ -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) {
@ -569,7 +553,7 @@ func Test_caHandler_Sign(t *testing.T) {
tests := []struct {
name string
input string
claims []Claim
certAttrOpts []interface{}
autherr error
cert *x509.Certificate
root *x509.Certificate
@ -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",

View file

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

View file

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

View file

@ -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 {
auth *Authority
ott string
err *apiError
claims []api.Claim
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{
auth: a,
ott: "foo",
err: &apiError{errors.New("error parsing OTT"),
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{
auth: a,
ott: raw,
err: &apiError{errors.New("error validating OTT"),
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{
auth: a,
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}},
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{
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{
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{
auth: a,
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)
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))
}
}
})

View file

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

View file

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

View file

@ -6,14 +6,14 @@ 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{
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",
@ -22,19 +22,27 @@ var DefaultTLSOptions = tlsutil.TLSOptions{
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
}
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"`
Provisioners []*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"`
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
}
}

View file

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

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 (
"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())

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"))

3
ca/testdata/ca.json vendored
View file

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

View file

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

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
}