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