forked from TrueCloudLab/certificates
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:
parent
d7c31c3133
commit
ee7db4006a
20 changed files with 620 additions and 430 deletions
71
api/api.go
71
api/api.go
|
@ -11,43 +11,19 @@ import (
|
|||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/ca-component/provisioner"
|
||||
"github.com/smallstep/ca-component/authority"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
"github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
||||
// Minimum and maximum validity of an end-entity (not root or intermediate) certificate.
|
||||
// They will be overwritten with the values configured in the authority
|
||||
var (
|
||||
minCertDuration = 5 * time.Minute
|
||||
maxCertDuration = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Claim interface is implemented by types used to validate specific claims in a
|
||||
// certificate request.
|
||||
// TODO(mariano): Rename?
|
||||
type Claim interface {
|
||||
Valid(cr *x509.CertificateRequest) error
|
||||
}
|
||||
|
||||
// SignOptions contains the options that can be passed to the Authority.Sign
|
||||
// method.
|
||||
type SignOptions struct {
|
||||
NotAfter time.Time `json:"notAfter"`
|
||||
NotBefore time.Time `json:"notBefore"`
|
||||
}
|
||||
|
||||
// Authority is the interface implemented by a CA authority.
|
||||
type Authority interface {
|
||||
Authorize(ott string) ([]Claim, error)
|
||||
Authorize(ott string) ([]interface{}, error)
|
||||
GetTLSOptions() *tlsutil.TLSOptions
|
||||
GetMinDuration() time.Duration
|
||||
GetMaxDuration() time.Duration
|
||||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts SignOptions, claims ...Claim) (*x509.Certificate, *x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, signOpts authority.SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error)
|
||||
Renew(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
||||
GetProvisioners() ([]*provisioner.Provisioner, error)
|
||||
GetProvisioners() ([]*authority.Provisioner, error)
|
||||
GetEncryptedKey(kid string) (string, error)
|
||||
}
|
||||
|
||||
|
@ -176,7 +152,7 @@ type SignRequest struct {
|
|||
// ProvisionersResponse is the response object that returns the list of
|
||||
// provisioners.
|
||||
type ProvisionersResponse struct {
|
||||
Provisioners []*provisioner.Provisioner `json:"provisioners"`
|
||||
Provisioners []*authority.Provisioner `json:"provisioners"`
|
||||
}
|
||||
|
||||
// JWKSetByIssuerResponse is the response object that returns the map of
|
||||
|
@ -204,27 +180,6 @@ func (s *SignRequest) Validate() error {
|
|||
return BadRequest(errors.New("missing ott"))
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if s.NotBefore.IsZero() {
|
||||
s.NotBefore = now
|
||||
}
|
||||
if s.NotAfter.IsZero() {
|
||||
s.NotAfter = now.Add(x509util.DefaultCertValidity)
|
||||
}
|
||||
|
||||
if s.NotAfter.Before(now) {
|
||||
return BadRequest(errors.New("notAfter < now"))
|
||||
}
|
||||
if s.NotAfter.Before(s.NotBefore) {
|
||||
return BadRequest(errors.New("notAfter < notBefore"))
|
||||
}
|
||||
requestedDuration := s.NotAfter.Sub(s.NotBefore)
|
||||
if requestedDuration < minCertDuration {
|
||||
return BadRequest(errors.New("requested certificate validity duration is too short"))
|
||||
}
|
||||
if requestedDuration > maxCertDuration {
|
||||
return BadRequest(errors.New("requested certificate validity duration is too long"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -243,8 +198,6 @@ type caHandler struct {
|
|||
|
||||
// New creates a new RouterHandler with the CA endpoints.
|
||||
func New(authority Authority) RouterHandler {
|
||||
minCertDuration = authority.GetMinDuration()
|
||||
maxCertDuration = authority.GetMaxDuration()
|
||||
return &caHandler{
|
||||
Authority: authority,
|
||||
}
|
||||
|
@ -296,18 +249,18 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
claims, err := h.Authority.Authorize(body.OTT)
|
||||
signOpts := authority.SignOptions{
|
||||
NotBefore: body.NotBefore,
|
||||
NotAfter: body.NotAfter,
|
||||
}
|
||||
|
||||
extraOpts, err := h.Authority.Authorize(body.OTT)
|
||||
if err != nil {
|
||||
WriteError(w, Unauthorized(err))
|
||||
return
|
||||
}
|
||||
|
||||
opts := SignOptions{
|
||||
NotBefore: body.NotBefore,
|
||||
NotAfter: body.NotAfter,
|
||||
}
|
||||
|
||||
cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, claims...)
|
||||
cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, signOpts, extraOpts...)
|
||||
if err != nil {
|
||||
WriteError(w, Forbidden(err))
|
||||
return
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
133
authority/provisioner.go
Normal 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
|
||||
}
|
|
@ -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())
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
113
authority/tls.go
113
authority/tls.go
|
@ -3,42 +3,58 @@ package authority
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/ca-component/api"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
stepx509 "github.com/smallstep/cli/pkg/x509"
|
||||
)
|
||||
|
||||
// GetMinDuration returns the minimum validity of an end-entity (not root or
|
||||
// intermediate) certificate.
|
||||
func (a *Authority) GetMinDuration() time.Duration {
|
||||
if a.config.AuthorityConfig.MinCertDuration == nil {
|
||||
return minCertDuration
|
||||
}
|
||||
return a.config.AuthorityConfig.MinCertDuration.Duration
|
||||
}
|
||||
|
||||
// GetMaxDuration returns the maximum validity of an end-entity (not root or
|
||||
// intermediate) certificate.
|
||||
func (a *Authority) GetMaxDuration() time.Duration {
|
||||
if a.config.AuthorityConfig.MaxCertDuration == nil {
|
||||
return maxCertDuration
|
||||
}
|
||||
return a.config.AuthorityConfig.MaxCertDuration.Duration
|
||||
}
|
||||
|
||||
// GetTLSOptions returns the tls options configured.
|
||||
func (a *Authority) GetTLSOptions() *tlsutil.TLSOptions {
|
||||
return a.config.TLS
|
||||
}
|
||||
|
||||
// SignOptions contains the options that can be passed to the Authority.Sign
|
||||
// method.
|
||||
type SignOptions struct {
|
||||
NotAfter time.Time `json:"notAfter"`
|
||||
NotBefore time.Time `json:"notBefore"`
|
||||
}
|
||||
|
||||
func withIssuerAlternativeNameExtension(name string) x509util.WithOption {
|
||||
return func(p x509util.Profile) error {
|
||||
crt := p.Subject()
|
||||
|
||||
iatExt := []asn1.RawValue{
|
||||
asn1.RawValue{
|
||||
Tag: 2,
|
||||
Class: 2,
|
||||
Bytes: []byte(name),
|
||||
},
|
||||
}
|
||||
iatExtBytes, err := asn1.Marshal(iatExt)
|
||||
if err != nil {
|
||||
return &apiError{err, http.StatusInternalServerError, nil}
|
||||
}
|
||||
|
||||
crt.ExtraExtensions = append(crt.ExtraExtensions, pkix.Extension{
|
||||
Id: []int{2, 5, 9, 18},
|
||||
Critical: false,
|
||||
Value: iatExtBytes,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
|
||||
return func(p x509util.Profile) error {
|
||||
if def == nil {
|
||||
|
@ -70,17 +86,40 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
|
|||
}
|
||||
|
||||
// Sign creates a signed certificate from a certificate signing request.
|
||||
func (a *Authority) Sign(csr *x509.CertificateRequest, opts api.SignOptions, claims ...api.Claim) (*x509.Certificate, *x509.Certificate, error) {
|
||||
if err := ValidateClaims(csr, claims); err != nil {
|
||||
return nil, nil, &apiError{err, http.StatusUnauthorized, context{}}
|
||||
func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error) {
|
||||
var (
|
||||
errContext = context{"csr": csr, "signOptions": signOpts}
|
||||
claims = []certClaim{}
|
||||
mods = []x509util.WithOption{}
|
||||
)
|
||||
for _, op := range extraOpts {
|
||||
switch k := op.(type) {
|
||||
case certClaim:
|
||||
claims = append(claims, k)
|
||||
case x509util.WithOption:
|
||||
mods = append(mods, k)
|
||||
case *Provisioner:
|
||||
m, c, err := k.getTLSApps(signOpts)
|
||||
if err != nil {
|
||||
return nil, nil, &apiError{err, http.StatusInternalServerError, errContext}
|
||||
}
|
||||
mods = append(mods, m...)
|
||||
mods = append(mods, []x509util.WithOption{
|
||||
x509util.WithHosts(csr.Subject.CommonName),
|
||||
withDefaultASN1DN(a.config.AuthorityConfig.Template),
|
||||
}...)
|
||||
claims = append(claims, c...)
|
||||
default:
|
||||
return nil, nil, &apiError{errors.Errorf("sign: invalid extra option type %T", k),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
}
|
||||
|
||||
stepCSR, err := stepx509.ParseCertificateRequest(csr.Raw)
|
||||
if err != nil {
|
||||
return nil, nil, &apiError{errors.Wrap(err, "error converting x509 csr to stepx509 csr"),
|
||||
http.StatusInternalServerError, context{}}
|
||||
return nil, nil, &apiError{errors.Wrap(err, "sign: error converting x509 csr to stepx509 csr"),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
|
||||
// DNSNames and IPAddresses are validated but to avoid duplications we will
|
||||
// clean them as x509util.NewLeafProfileWithCSR will set the right values.
|
||||
stepCSR.DNSNames = nil
|
||||
|
@ -88,27 +127,31 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, opts api.SignOptions, cla
|
|||
|
||||
issIdentity := a.intermediateIdentity
|
||||
leaf, err := x509util.NewLeafProfileWithCSR(stepCSR, issIdentity.Crt,
|
||||
issIdentity.Key, x509util.WithHosts(csr.Subject.CommonName),
|
||||
x509util.WithNotBeforeAfter(opts.NotBefore, opts.NotAfter),
|
||||
withDefaultASN1DN(a.config.AuthorityConfig.Template))
|
||||
issIdentity.Key, mods...)
|
||||
if err != nil {
|
||||
return nil, nil, &apiError{err, http.StatusInternalServerError, context{}}
|
||||
return nil, nil, &apiError{errors.Wrapf(err, "sign"), http.StatusInternalServerError, errContext}
|
||||
}
|
||||
|
||||
if err := validateClaims(leaf.Subject(), claims); err != nil {
|
||||
return nil, nil, &apiError{errors.Wrapf(err, "sign"), http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
crtBytes, err := leaf.CreateCertificate()
|
||||
if err != nil {
|
||||
return nil, nil, &apiError{errors.Wrap(err, "error creating new leaf certificate from input csr"),
|
||||
http.StatusInternalServerError, context{}}
|
||||
return nil, nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
|
||||
serverCert, err := x509.ParseCertificate(crtBytes)
|
||||
if err != nil {
|
||||
return nil, nil, &apiError{errors.Wrap(err, "error parsing new server certificate"),
|
||||
http.StatusInternalServerError, context{}}
|
||||
return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
|
||||
caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw)
|
||||
if err != nil {
|
||||
return nil, nil, &apiError{errors.Wrap(err, "error parsing intermediate certificate"),
|
||||
http.StatusInternalServerError, context{}}
|
||||
return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
|
||||
return serverCert, caCert, nil
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
3
ca/testdata/ca.json
vendored
|
@ -70,6 +70,9 @@
|
|||
"alg": "ES256",
|
||||
"x": "tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y",
|
||||
"y": "e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA"
|
||||
},
|
||||
"claims": {
|
||||
"minTLSCertDuration": "30s"
|
||||
}
|
||||
}, {
|
||||
"issuer": "mariano",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue