Add x5c provisioner capabilities

This commit is contained in:
max furman 2019-09-04 18:31:09 -07:00
parent 2781045524
commit d368791606
32 changed files with 1805 additions and 246 deletions

View file

@ -51,6 +51,7 @@ linters:
- deadcode
- staticcheck
- unused
- gosimple
run:
skip-dirs:

View file

@ -23,9 +23,6 @@
# non-go = false
# go-tests = true
# unused-packages = true
[[override]]
name = "gopkg.in/alecthomas/kingpin.v3-unstable"
revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
[[constraint]]
branch = "master"

View file

@ -128,7 +128,6 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink,
acme.URLSafeProvisionerName(prov), true, acc.GetID()))
api.JSONStatus(w, acc, httpStatus)
return
}
// GetUpdateAccount is the api for updating an ACME account.
@ -172,7 +171,6 @@ func (h *Handler) GetUpdateAccount(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink, acme.URLSafeProvisionerName(prov), true, acc.GetID()))
api.JSON(w, acc)
return
}
func logOrdersByAccount(w http.ResponseWriter, oids []string) {
@ -209,5 +207,4 @@ func (h *Handler) GetOrdersByAccount(w http.ResponseWriter, r *http.Request) {
}
api.JSON(w, orders)
logOrdersByAccount(w, orders)
return
}

View file

@ -113,7 +113,6 @@ func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) {
} else {
w.WriteHeader(http.StatusNoContent)
}
return
}
// GetDirectory is the ACME resource for returning a directory configuration
@ -126,7 +125,6 @@ func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) {
}
dir := h.Auth.GetDirectory(prov)
api.JSON(w, dir)
return
}
// GetAuthz ACME api for retrieving an Authz.
@ -149,7 +147,6 @@ func (h *Handler) GetAuthz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", h.Auth.GetLink(acme.AuthzLink, acme.URLSafeProvisionerName(prov), true, authz.GetID()))
api.JSON(w, authz)
return
}
// GetChallenge ACME api for retrieving a Challenge.
@ -191,7 +188,6 @@ func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Link", link(getLink(acme.AuthzLink, acme.URLSafeProvisionerName(prov), true, ch.GetAuthzID()), "up"))
w.Header().Set("Location", getLink(acme.ChallengeLink, acme.URLSafeProvisionerName(prov), true, ch.GetID()))
api.JSON(w, ch)
return
}
// GetCertificate ACME api for retrieving a Certificate.
@ -210,5 +206,4 @@ func (h *Handler) GetCertificate(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8")
w.Write(certBytes)
return
}

View file

@ -42,7 +42,6 @@ func (h *Handler) addNonce(next nextHTTP) nextHTTP {
w.Header().Set("Cache-Control", "no-store")
logNonce(w, nonce)
next(w, r)
return
}
}
@ -57,7 +56,6 @@ func (h *Handler) addDirLink(next nextHTTP) nextHTTP {
}
w.Header().Add("Link", link(h.Auth.GetLink(acme.DirectoryLink, acme.URLSafeProvisionerName(prov), true), "index"))
next(w, r)
return
}
}
@ -87,7 +85,6 @@ func (h *Handler) verifyContentType(next nextHTTP) nextHTTP {
}
api.WriteError(w, acme.MalformedErr(errors.Errorf(
"expected content-type to be in %s, but got %s", expected, ct)))
return
}
}
@ -106,7 +103,6 @@ func (h *Handler) parseJWS(next nextHTTP) nextHTTP {
}
ctx := context.WithValue(r.Context(), jwsContextKey, jws)
next(w, r.WithContext(ctx))
return
}
}
@ -202,7 +198,6 @@ func (h *Handler) validateJWS(next nextHTTP) nextHTTP {
return
}
next(w, r)
return
}
}
@ -248,7 +243,6 @@ func (h *Handler) extractJWK(next nextHTTP) nextHTTP {
ctx = context.WithValue(ctx, accContextKey, acc)
}
next(w, r.WithContext(ctx))
return
}
}
@ -275,7 +269,6 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
}
ctx = context.WithValue(ctx, provisionerContextKey, p)
next(w, r.WithContext(ctx))
return
}
}
@ -355,7 +348,6 @@ func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {
isEmptyJSON: string(payload) == "{}",
})
next(w, r.WithContext(ctx))
return
}
}
@ -372,6 +364,5 @@ func (h *Handler) isPostAsGet(next nextHTTP) nextHTTP {
return
}
next(w, r)
return
}
}

View file

@ -26,7 +26,6 @@ var testBody = []byte("foo")
func testNext(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
return
}
func TestHandlerAddNonce(t *testing.T) {
@ -471,7 +470,6 @@ func TestHandlerParseJWS(t *testing.T) {
assert.FatalError(t, err)
assert.Equals(t, gotRaw, expRaw)
w.Write(testBody)
return
},
statusCode: 200,
}
@ -923,7 +921,6 @@ func TestHandlerLookupJWK(t *testing.T) {
assert.FatalError(t, err)
assert.Equals(t, _jwk, jwk)
w.Write(testBody)
return
},
statusCode: 200,
}
@ -1114,7 +1111,6 @@ func TestHandlerExtractJWK(t *testing.T) {
assert.FatalError(t, err)
assert.Equals(t, _jwk.KeyID, pub.KeyID)
w.Write(testBody)
return
},
statusCode: 200,
}
@ -1139,7 +1135,6 @@ func TestHandlerExtractJWK(t *testing.T) {
assert.FatalError(t, err)
assert.Equals(t, _jwk.KeyID, pub.KeyID)
w.Write(testBody)
return
},
statusCode: 200,
}
@ -1448,7 +1443,6 @@ func TestHandlerValidateJWS(t *testing.T) {
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
return
},
statusCode: 200,
}
@ -1479,7 +1473,6 @@ func TestHandlerValidateJWS(t *testing.T) {
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
return
},
statusCode: 200,
}
@ -1510,7 +1503,6 @@ func TestHandlerValidateJWS(t *testing.T) {
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
return
},
statusCode: 200,
}

View file

@ -97,7 +97,6 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID()))
api.JSONStatus(w, o, http.StatusCreated)
return
}
// GetOrder ACME api for retrieving an order.
@ -121,7 +120,6 @@ func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID()))
api.JSON(w, o)
return
}
// FinalizeOrder attemptst to finalize an order and create a certificate.
@ -160,5 +158,4 @@ func (h *Handler) FinalizeOrder(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.ID))
api.JSON(w, o)
return
}

View file

@ -69,10 +69,12 @@ func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error
return nil, errors.Errorf("unexpected method type %d in context", m)
}
return []SignOption{
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// modifiers / withOptions
newProvisionerExtensionOption(TypeACME, p.Name, ""),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
}, nil
}

View file

@ -155,28 +155,23 @@ func TestACME_AuthorizeSign(t *testing.T) {
if assert.NotNil(t, got) {
assert.Len(t, 4, got)
_pdd := got[0]
pdd, ok := _pdd.(profileDefaultDuration)
assert.True(t, ok)
assert.Equals(t, pdd, profileDefaultDuration(86400000000000))
_peo := got[1]
peo, ok := _peo.(*provisionerExtensionOption)
assert.True(t, ok)
assert.Equals(t, peo.Type, 6)
assert.Equals(t, peo.Name, "test@acme-provisioner.com")
assert.Equals(t, peo.CredentialID, "")
assert.Equals(t, peo.KeyValuePairs, nil)
_vv := got[2]
vv, ok := _vv.(*validityValidator)
assert.True(t, ok)
assert.Equals(t, vv.min, time.Duration(300000000000))
assert.Equals(t, vv.max, time.Duration(86400000000000))
_dpkv := got[3]
_, ok = _dpkv.(defaultPublicKeyValidator)
assert.True(t, ok)
for _, o := range got {
switch v := o.(type) {
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeACME))
assert.Equals(t, v.Name, tt.prov.GetName())
assert.Equals(t, v.CredentialID, "")
assert.Len(t, 0, v.KeyValuePairs)
case profileDefaultDuration:
assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration())
case defaultPublicKeyValidator:
case *validityValidator:
assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration())
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
}
}
}
})

View file

@ -274,8 +274,8 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
if p.claimer.IsSSHCAEnabled() == false {
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(payload)
@ -296,10 +296,12 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}
return append(so,
// modifiers / withOptions
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},
commonNameValidator(payload.Claims.Subject),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
), nil
}
@ -466,13 +468,15 @@ func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) {
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
return append(signOptions,
// set the default extensions
// Set the default extensions.
&sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer},
// validate public key
// Set the validity bounds if not set.
sshDefaultValidityModifier(p.claimer),
// Validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate
// Validate the validity period.
&sshCertificateValidityValidator{p.claimer},
// Require all the fields in the SSH certificate
&sshCertificateDefaultValidator{},
), nil
}

View file

@ -266,8 +266,8 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
}
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
if p.claimer.IsSSHCAEnabled() == false {
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims, name)
@ -284,9 +284,11 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
}
return append(so,
defaultPublicKeyValidator{},
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// modifiers / withOptions
newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
), nil
}
@ -323,13 +325,15 @@ func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
return append(signOptions,
// set the default extensions
// Set the default extensions.
&sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer},
// validate public key
// Set the validity bounds if not set.
sshDefaultValidityModifier(p.claimer),
// Validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate
// Validate the validity period.
&sshCertificateValidityValidator{p.claimer},
// Require all the fields in the SSH certificate
&sshCertificateDefaultValidator{},
), nil
}

View file

@ -129,6 +129,8 @@ func (c *Collection) LoadByCertificate(cert *x509.Certificate) (Interface, bool)
return c.Load("gcp/" + string(provisioner.Name))
case TypeACME:
return c.Load("acme/" + string(provisioner.Name))
case TypeX5C:
return c.Load("x5c/" + string(provisioner.Name))
default:
return c.Load(string(provisioner.CredentialID))
}

View file

@ -213,8 +213,8 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
if p.claimer.IsSSHCAEnabled() == false {
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims)
@ -237,9 +237,11 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}
return append(so,
defaultPublicKeyValidator{},
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// modifiers / withOptions
newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
), nil
}
@ -378,13 +380,15 @@ func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) {
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
return append(signOptions,
// set the default extensions
// Set the default extensions
&sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer},
// validate public key
// Set the validity bounds if not set.
sshDefaultValidityModifier(p.claimer),
// Validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate
// Validate the validity period.
&sshCertificateValidityValidator{p.claimer},
// Require all the fields in the SSH certificate
&sshCertificateDefaultValidator{},
), nil
}

View file

@ -141,9 +141,9 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
return nil, err
}
// Check for SSH token
if claims.Step != nil && claims.Step.SSH != nil {
if p.claimer.IsSSHCAEnabled() == false {
// Check for SSH sign-ing request.
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims)
@ -158,13 +158,15 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
dnsNames, ips, emails := x509util.SplitSANs(claims.SANs)
return []SignOption{
defaultPublicKeyValidator{},
commonNameValidator(claims.Subject),
dnsNamesValidator(dnsNames),
ipAddressesValidator(ips),
emailAddressesValidator(emails),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// modifiers / withOptions
newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
// validators
commonNameValidator(claims.Subject),
defaultPublicKeyValidator{},
dnsNamesValidator(dnsNames),
emailAddressesValidator(emails),
ipAddressesValidator(ips),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
}, nil
}
@ -180,6 +182,9 @@ func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error {
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
t := now()
if claims.Step == nil || claims.Step.SSH == nil {
return nil, errors.New("authorization token must be an SSH provisioning token")
}
opts := claims.Step.SSH
signOptions := []SignOption{
// validates user's SSHOptions with the ones in the token
@ -206,13 +211,15 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert})
return append(signOptions,
// set the default extensions
// Set the default extensions.
&sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer},
// validate public key
// Set the validity bounds if not set.
sshDefaultValidityModifier(p.claimer),
// Validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate
// Validate the validity period.
&sshCertificateValidityValidator{p.claimer},
// Require and validate all the default fields in the SSH certificate.
&sshCertificateDefaultValidator{},
), nil
}

View file

@ -6,33 +6,16 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors"
"net"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/cli/jose"
)
var (
defaultDisableRenewal = false
defaultEnableSSHCA = true
globalProvisionerClaims = Claims{
MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour},
DisableRenewal: &defaultDisableRenewal,
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &defaultEnableSSHCA,
}
)
func TestJWK_Getters(t *testing.T) {
p, err := generateJWK()
assert.FatalError(t, err)
@ -247,7 +230,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
key1, err := decryptJSONWebKey(p1.EncryptedKey)
assert.FatalError(t, err)
t1, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key1)
t1, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{"127.0.0.1", "max@smallstep.com", "foo"}, time.Now(), key1)
assert.FatalError(t, err)
t2, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{}, time.Now(), key1)
@ -260,14 +243,17 @@ func TestJWK_AuthorizeSign(t *testing.T) {
token string
}
tests := []struct {
name string
prov *JWK
args args
err error
name string
prov *JWK
args args
err error
dns []string
emails []string
ips []net.IP
}{
{"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
{"ok-sans", p1, args{t1}, nil},
{"ok-no-sans", p1, args{t2}, nil},
{name: "fail-signature", prov: p1, args: args{failSig}, err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
{"ok-sans", p1, args{t1}, nil, []string{"foo"}, []string{"max@smallstep.com"}, []net.IP{net.ParseIP("127.0.0.1")}},
{"ok-no-sans", p1, args{t2}, nil, []string{"subject"}, []string{}, []net.IP{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -279,19 +265,30 @@ func TestJWK_AuthorizeSign(t *testing.T) {
} else {
if assert.NotNil(t, got) {
assert.Len(t, 8, got)
_cnv := got[1]
cnv, ok := _cnv.(commonNameValidator)
assert.True(t, ok)
assert.Equals(t, string(cnv), "subject")
_dnv := got[2]
dnv, ok := _dnv.(dnsNamesValidator)
assert.True(t, ok)
if tt.name == "ok-sans" {
assert.Equals(t, []string(dnv), []string{"test.smallstep.com"})
} else {
assert.Equals(t, []string(dnv), []string{"subject"})
for _, o := range got {
switch v := o.(type) {
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeJWK))
assert.Equals(t, v.Name, tt.prov.GetName())
assert.Equals(t, v.CredentialID, tt.prov.Key.KeyID)
assert.Len(t, 0, v.KeyValuePairs)
case profileDefaultDuration:
assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration())
case commonNameValidator:
assert.Equals(t, string(v), "subject")
case defaultPublicKeyValidator:
case dnsNamesValidator:
assert.Equals(t, []string(v), tt.dns)
case emailAddressesValidator:
assert.Equals(t, []string(v), tt.emails)
case ipAddressesValidator:
assert.Equals(t, []net.IP(v), tt.ips)
case *validityValidator:
assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration())
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
}
}
}

View file

@ -285,26 +285,19 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
}
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
if o.claimer.IsSSHCAEnabled() == false {
if MethodFromContext(ctx) == SignSSHMethod {
if !o.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID())
}
return o.authorizeSSHSign(claims)
}
// Admins should be able to authorize any SAN
if o.IsAdmin(claims.Email) {
return []SignOption{
profileDefaultDuration(o.claimer.DefaultTLSCertDuration()),
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()),
}, nil
}
so := []SignOption{
defaultPublicKeyValidator{},
profileDefaultDuration(o.claimer.DefaultTLSCertDuration()),
// modifiers / withOptions
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
profileDefaultDuration(o.claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},
newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()),
}
// Admins should be able to authorize any SAN
@ -350,13 +343,15 @@ func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) {
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
return append(signOptions,
// set the default extensions
// Set the default extensions
&sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{o.claimer},
// validate public key
// Set the validity bounds if not set.
sshDefaultValidityModifier(o.claimer),
// Validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate
// Validate the validity period.
&sshCertificateValidityValidator{o.claimer},
// Require all the fields in the SSH certificate
&sshCertificateDefaultValidator{},
), nil
}

View file

@ -11,6 +11,7 @@ import (
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/cli/jose"
)
@ -298,11 +299,31 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
if err != nil {
assert.Nil(t, got)
} else {
assert.NotNil(t, got)
if tt.name == "admin" {
assert.Len(t, 3, got)
} else {
assert.Len(t, 5, got)
if assert.NotNil(t, got) {
if tt.name == "admin" {
assert.Len(t, 4, got)
} else {
assert.Len(t, 5, got)
}
for _, o := range got {
switch v := o.(type) {
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeOIDC))
assert.Equals(t, v.Name, tt.prov.GetName())
assert.Equals(t, v.CredentialID, tt.prov.ClientID)
assert.Len(t, 0, v.KeyValuePairs)
case profileDefaultDuration:
assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration())
case defaultPublicKeyValidator:
case *validityValidator:
assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration())
case emailOnlyIdentity:
assert.Equals(t, string(v), "name@smallstep.com")
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
}
}
}
})

View file

@ -86,6 +86,8 @@ const (
TypeAzure Type = 5
// TypeACME is used to indicate the ACME provisioners.
TypeACME Type = 6
// TypeX5C is used to indicate the X5C provisioners.
TypeX5C Type = 7
// RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map.
RevokeAudienceKey = "revoke"
@ -108,6 +110,8 @@ func (t Type) String() string {
return "Azure"
case TypeACME:
return "ACME"
case TypeX5C:
return "X5C"
default:
return ""
}
@ -157,6 +161,8 @@ func (l *List) UnmarshalJSON(data []byte) error {
p = &Azure{}
case "acme":
p = &ACME{}
case "x5c":
p = &X5C{}
default:
// Skip unsupported provisioners. A client using this method may be
// compiled with a version of smallstep/certificates that does not

View file

@ -53,19 +53,6 @@ func (v profileWithOption) Option(Options) x509util.WithOption {
return x509util.WithOption(v)
}
// profileDefaultDuration is a wrapper against x509util.WithOption to conform the
// interface.
type profileDefaultDuration time.Duration
func (v profileDefaultDuration) Option(so Options) x509util.WithOption {
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = time.Now()
}
notAfter := so.NotAfter.RelativeTime(notBefore)
return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v))
}
// emailOnlyIdentity is a CertificateRequestValidator that checks that the only
// SAN provided is the given email address.
type emailOnlyIdentity string
@ -197,7 +184,61 @@ func (v emailAddressesValidator) Valid(req *x509.CertificateRequest) error {
return nil
}
// validityValidator validates the certificate temporal validity settings.
// profileDefaultDuration is a wrapper against x509util.WithOption to conform
// the SignOption interface.
type profileDefaultDuration time.Duration
func (v profileDefaultDuration) Option(so Options) x509util.WithOption {
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = time.Now()
}
notAfter := so.NotAfter.RelativeTime(notBefore)
return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v))
}
// profileLimitDuration is an x509 profile option that modifies an x509 validity
// period according to an imposed expiration time.
type profileLimitDuration struct {
def time.Duration
notAfter time.Time
}
// Option returns an x509util option that limits the validity period of a
// certificate to one that is superficially imposed.
func (v profileLimitDuration) Option(so Options) x509util.WithOption {
return func(p x509util.Profile) error {
n := now()
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = n
}
if notBefore.After(v.notAfter) {
return errors.Errorf("provisioning credential expiration (%s) is before "+
"requested certificate notBefore (%s)", v.notAfter, notBefore)
}
notAfter := so.NotAfter.RelativeTime(notBefore)
if notAfter.After(v.notAfter) {
return errors.Errorf("provisioning credential expiration (%s) is before "+
"requested certificate notAfter (%s)", v.notAfter, notBefore)
}
if notAfter.IsZero() {
t := notBefore.Add(v.def)
if t.After(v.notAfter) {
notAfter = v.notAfter
} else {
notAfter = t
}
}
crt := p.Subject()
crt.NotBefore = notBefore
crt.NotAfter = notAfter
return nil
}
}
// validityValidator validates the certificate validity settings.
type validityValidator struct {
min time.Duration
max time.Duration
@ -208,7 +249,8 @@ func newValidityValidator(min, max time.Duration) *validityValidator {
return &validityValidator{min: min, max: max}
}
// Validate validates the certificate temporal validity settings.
// Valid validates the certificate validity settings (notBefore/notAfter) and
// and total duration.
func (v *validityValidator) Valid(crt *x509.Certificate) error {
var (
na = crt.NotAfter

View file

@ -273,3 +273,87 @@ func Test_validityValidator_Valid(t *testing.T) {
})
}
}
func Test_profileLimitDuration_Option(t *testing.T) {
n := now()
type test struct {
pld profileLimitDuration
so Options
cert *x509.Certificate
valid func(*x509.Certificate)
err error
}
tests := map[string]func() test{
"fail/notBefore-after-limit": func() test {
d, err := ParseTimeDuration("8h")
assert.FatalError(t, err)
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: d},
cert: new(x509.Certificate),
err: errors.New("provisioning credential expiration ("),
}
},
"fail/requested-notAfter-after-limit": func() test {
d, err := ParseTimeDuration("4h")
assert.FatalError(t, err)
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d},
cert: new(x509.Certificate),
err: errors.New("provisioning credential expiration ("),
}
},
"ok/valid-notAfter-requested": func() test {
d, err := ParseTimeDuration("2h")
assert.FatalError(t, err)
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))
assert.Equals(t, cert.NotAfter, n.Add(5*time.Hour))
},
}
},
"ok/valid-notAfter-nil-limit-over-default": func() test {
return test{
pld: profileLimitDuration{def: 1 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))
assert.Equals(t, cert.NotAfter, n.Add(4*time.Hour))
},
}
},
"ok/valid-notAfter-nil-limit-under-default": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))
assert.Equals(t, cert.NotAfter, n.Add(6*time.Hour))
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
prof := &x509util.Leaf{}
prof.SetSubject(tt.cert)
if err := tt.pld.Option(tt.so)(prof); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
} else {
if assert.Nil(t, tt.err) {
tt.valid(prof.Subject())
}
}
})
}
}

View file

@ -191,47 +191,67 @@ func (m *sshDefaultExtensionModifier) Modify(cert *ssh.Certificate) error {
}
}
// sshCertificateValidityModifier is a SSHCertificateModifier checks the
// sshValidityModifier is an SSHCertificateModifier that checks the
// validity bounds, setting them if they are not provided. It will fail if a
// CertType has not been set or is not valid.
type sshCertificateValidityModifier struct {
type sshValidityModifier struct {
*Claimer
validBefore time.Time
}
func (m *sshCertificateValidityModifier) Modify(cert *ssh.Certificate) error {
var d, min, max time.Duration
func (m *sshValidityModifier) Modify(cert *ssh.Certificate) error {
var d time.Duration
switch cert.CertType {
case ssh.UserCert:
d = m.DefaultUserSSHCertDuration()
min = m.MinUserSSHCertDuration()
max = m.MaxUserSSHCertDuration()
case ssh.HostCert:
d = m.DefaultHostSSHCertDuration()
min = m.MinHostSSHCertDuration()
max = m.MaxHostSSHCertDuration()
case 0:
return errors.New("ssh certificate type has not been set")
default:
return errors.Errorf("unknown ssh certificate type %d", cert.CertType)
}
hasLimit := !m.validBefore.IsZero()
n := now()
if cert.ValidAfter == 0 {
cert.ValidAfter = uint64(now().Truncate(time.Second).Unix())
cert.ValidAfter = uint64(n.Truncate(time.Second).Unix())
}
if cert.ValidBefore == 0 {
t := time.Unix(int64(cert.ValidAfter), 0)
cert.ValidBefore = uint64(t.Add(d).Unix())
certValidAfter := time.Unix(int64(cert.ValidAfter), 0)
if hasLimit && certValidAfter.After(m.validBefore) {
return errors.Errorf("provisioning credential expiration (%s) is before "+
"requested certificate validAfter (%s)", m.validBefore, certValidAfter)
}
diff := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second
switch {
case diff < min:
return errors.Errorf("ssh certificate duration cannot be lower than %s", min)
case diff > max:
return errors.Errorf("ssh certificate duration cannot be greater than %s", max)
default:
return nil
if cert.ValidBefore == 0 {
certValidBefore := certValidAfter.Add(d)
if hasLimit && m.validBefore.Before(certValidBefore) {
certValidBefore = m.validBefore
}
cert.ValidBefore = uint64(certValidBefore.Unix())
} else if hasLimit {
certValidBefore := time.Unix(int64(cert.ValidBefore), 0)
if m.validBefore.Before(certValidBefore) {
return errors.Errorf("provisioning credential expiration (%s) is before "+
"requested certificate validBefore (%s)", m.validBefore, certValidBefore)
}
}
return nil
}
func sshDefaultValidityModifier(c *Claimer) SSHCertificateModifier {
return &sshValidityModifier{c, time.Time{}}
}
// sshLimitValidityModifier adjusts the duration to
// min(default, remaining provisioning credential duration).
// E.g. if the default is 12hrs but the remaining validity of the provisioning
// credential is only 4hrs, this option will set the value to 4hrs (the min of the two values).
func sshLimitValidityModifier(c *Claimer, validBefore time.Time) SSHCertificateModifier {
return &sshValidityModifier{c, validBefore}
}
// sshCertificateOptionsValidator validates the user SSHOptions with the ones
@ -245,6 +265,48 @@ func (v sshCertificateOptionsValidator) Valid(got SSHOptions) error {
return want.match(got)
}
type sshCertificateValidityValidator struct {
*Claimer
}
func (v *sshCertificateValidityValidator) Valid(cert *ssh.Certificate) error {
switch {
case cert.ValidAfter == 0:
return errors.New("ssh certificate validAfter cannot be 0")
case cert.ValidBefore < uint64(now().Unix()):
return errors.New("ssh certificate validBefore cannot be in the past")
case cert.ValidBefore < cert.ValidAfter:
return errors.New("ssh certificate validBefore cannot be before validAfter")
}
var min, max time.Duration
switch cert.CertType {
case ssh.UserCert:
min = v.MinUserSSHCertDuration()
max = v.MaxUserSSHCertDuration()
case ssh.HostCert:
min = v.MinHostSSHCertDuration()
max = v.MaxHostSSHCertDuration()
case 0:
return errors.New("ssh certificate type has not been set")
default:
return errors.Errorf("unknown ssh certificate type %d", cert.CertType)
}
// seconds
dur := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second
switch {
case dur < min:
return errors.Errorf("requested duration of %s is less than minimum "+
"accepted duration for selected provisioner of %s", dur, min)
case dur > max:
return errors.Errorf("requested duration of %s is greater than maximum "+
"accepted duration for selected provisioner of %s", dur, max)
default:
return nil
}
}
// sshCertificateDefaultValidator implements a simple validator for all the
// fields in the SSH certificate.
type sshCertificateDefaultValidator struct{}

View file

@ -190,3 +190,197 @@ func Test_sshCertificateDefaultValidator_Valid(t *testing.T) {
})
}
}
func Test_sshCertificateValidityValidator(t *testing.T) {
p, err := generateX5C(nil)
assert.FatalError(t, err)
v := sshCertificateValidityValidator{p.claimer}
n := now()
tests := []struct {
name string
cert *ssh.Certificate
err error
}{
{
"fail/validAfter-0",
&ssh.Certificate{CertType: ssh.UserCert},
errors.New("ssh certificate validAfter cannot be 0"),
},
{
"fail/validBefore-in-past",
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(-time.Minute).Unix())},
errors.New("ssh certificate validBefore cannot be in the past"),
},
{
"fail/validBefore-before-validAfter",
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Add(5 * time.Minute).Unix()), ValidBefore: uint64(now().Add(3 * time.Minute).Unix())},
errors.New("ssh certificate validBefore cannot be before validAfter"),
},
{
"fail/cert-type-not-set",
&ssh.Certificate{ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(10 * time.Minute).Unix())},
errors.New("ssh certificate type has not been set"),
},
{
"fail/unexpected-cert-type",
&ssh.Certificate{
CertType: 3,
ValidAfter: uint64(now().Unix()),
ValidBefore: uint64(now().Add(10 * time.Minute).Unix()),
},
errors.New("unknown ssh certificate type 3"),
},
{
"fail/duration<min",
&ssh.Certificate{
CertType: 1,
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(4 * time.Minute).Unix()),
},
errors.New("requested duration of 4m0s is less than minimum accepted duration for selected provisioner of 5m0s"),
},
{
"fail/duration>max",
&ssh.Certificate{
CertType: 1,
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(48 * time.Hour).Unix()),
},
errors.New("requested duration of 48h0m0s is greater than maximum accepted duration for selected provisioner of 24h0m0s"),
},
{
"ok",
&ssh.Certificate{
CertType: 1,
ValidAfter: uint64(now().Unix()),
ValidBefore: uint64(now().Add(8 * time.Hour).Unix()),
},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := v.Valid(tt.cert); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
} else {
assert.Nil(t, tt.err)
}
})
}
}
func Test_sshValidityModifier(t *testing.T) {
n := now()
p, err := generateX5C(nil)
assert.FatalError(t, err)
type test struct {
svm *sshValidityModifier
cert *ssh.Certificate
valid func(*ssh.Certificate)
err error
}
tests := map[string]func() test{
"fail/type-not-set": func() test {
return test{
svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)},
cert: &ssh.Certificate{
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(8 * time.Hour).Unix()),
},
err: errors.New("ssh certificate type has not been set"),
}
},
"fail/type-not-recognized": func() test {
return test{
svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)},
cert: &ssh.Certificate{
CertType: 4,
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(8 * time.Hour).Unix()),
},
err: errors.New("unknown ssh certificate type 4"),
}
},
"fail/requested-validAfter-after-limit": func() test {
return test{
svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)},
cert: &ssh.Certificate{
CertType: 1,
ValidAfter: uint64(n.Add(2 * time.Hour).Unix()),
ValidBefore: uint64(n.Add(8 * time.Hour).Unix()),
},
err: errors.Errorf("provisioning credential expiration ("),
}
},
"fail/requested-validBefore-after-limit": func() test {
return test{
svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)},
cert: &ssh.Certificate{
CertType: 1,
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(2 * time.Hour).Unix()),
},
err: errors.New("provisioning credential expiration ("),
}
},
"ok/valid-requested-validBefore": func() test {
va, vb := uint64(n.Unix()), uint64(n.Add(2*time.Hour).Unix())
return test{
svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)},
cert: &ssh.Certificate{
CertType: 1,
ValidAfter: va,
ValidBefore: vb,
},
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.ValidAfter, va)
assert.Equals(t, cert.ValidBefore, vb)
},
}
},
"ok/empty-requested-validBefore-limit-after-default": func() test {
va := uint64(n.Unix())
return test{
svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(5 * time.Hour)},
cert: &ssh.Certificate{
CertType: 1,
ValidAfter: va,
},
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.ValidAfter, va)
assert.Equals(t, cert.ValidBefore, uint64(n.Add(4*time.Hour).Unix()))
},
}
},
"ok/empty-requested-validBefore-limit-before-default": func() test {
va := uint64(n.Unix())
return test{
svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)},
cert: &ssh.Certificate{
CertType: 1,
ValidAfter: va,
},
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.ValidAfter, va)
assert.Equals(t, cert.ValidBefore, uint64(n.Add(3*time.Hour).Unix()))
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
if err := tt.svm.Modify(tt.cert); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
} else {
if assert.Nil(t, tt.err) {
tt.valid(tt.cert)
}
}
})
}
}

View file

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow
GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5
MDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr
VpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC
BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW
f4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp
czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN
wtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z
AvUgkUQ2G25NBRmX
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw
EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw
MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl
qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC
AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99
oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw
E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1
2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC
lgsqsR63is+0YQ==
-----END CERTIFICATE-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIALytC4LyTTAagMLMv+rzq2vtfhFkhuyBz4kqsnRs6zioAoGCCqGSM49
AwEHoUQDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvrVpgS
PXYruNRFduPWX564Abz/TDmb276JbKGeQg==
-----END EC PRIVATE KEY-----

View file

@ -19,9 +19,44 @@ import (
"github.com/smallstep/cli/jose"
)
var testAudiences = Audiences{
Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"},
Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"},
var (
defaultDisableRenewal = false
defaultEnableSSHCA = true
globalProvisionerClaims = Claims{
MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour},
DisableRenewal: &defaultDisableRenewal,
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &defaultEnableSSHCA,
}
testAudiences = Audiences{
Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"},
Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"},
}
)
func provisionerClaims() *Claims {
ddr := false
des := true
return &Claims{
MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour},
DisableRenewal: &ddr,
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &des,
}
}
const awsTestCertificate = `-----BEGIN CERTIFICATE-----
@ -162,6 +197,58 @@ func generateJWK() (*JWK, error) {
}, nil
}
func generateX5C(root []byte) (*X5C, error) {
if root == nil {
root = []byte(`-----BEGIN CERTIFICATE-----
MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES
MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz
OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB
BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO
BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T
AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD
VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH
1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY
M46l92gdOozT
-----END CERTIFICATE-----`)
}
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
rootPool := x509.NewCertPool()
var (
block *pem.Block
rest = root
)
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block")
}
rootPool.AddCert(cert)
}
return &X5C{
Name: name,
Type: "X5C",
Roots: root,
Claims: &globalProvisionerClaims,
audiences: testAudiences,
claimer: claimer,
rootPool: rootPool,
}, nil
}
func generateOIDC() (*OIDC, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
@ -446,11 +533,31 @@ func generateSimpleToken(iss, aud string, jwk *jose.JSONWebKey) (string, error)
return generateToken("subject", iss, aud, "name@smallstep.com", []string{"test.smallstep.com"}, time.Now(), jwk)
}
func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey) (string, error) {
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),
)
type tokOption func(*jose.SignerOptions) error
func withX5CHdr(certs []*x509.Certificate) tokOption {
return func(so *jose.SignerOptions) error {
strs := make([]string, len(certs))
for i, cert := range certs {
strs[i] = base64.StdEncoding.EncodeToString(cert.Raw)
}
so.WithHeader("x5c", strs)
return nil
}
}
func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", jwk.KeyID)
for _, o := range tokOpts {
if err := o(so); err != nil {
return "", err
}
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
@ -742,3 +849,24 @@ func generateACME() (*ACME, error) {
}
return p, nil
}
func parseCerts(b []byte) ([]*x509.Certificate, error) {
var (
block *pem.Block
rest = b
certs = []*x509.Certificate{}
)
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block")
}
certs = append(certs, cert)
}
return certs, nil
}

View file

@ -0,0 +1,267 @@
package provisioner
import (
"context"
"crypto/x509"
"encoding/pem"
"time"
"github.com/pkg/errors"
"github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose"
)
// x5cPayload extends jwt.Claims with step attributes.
type x5cPayload struct {
jose.Claims
SANs []string `json:"sans,omitempty"`
Step *stepPayload `json:"step,omitempty"`
chains [][]*x509.Certificate
}
// X5C is the default provisioner, an entity that can sign tokens necessary for
// signature requests.
type X5C struct {
Type string `json:"type"`
Name string `json:"name"`
Roots []byte `json:"roots"`
Claims *Claims `json:"claims,omitempty"`
claimer *Claimer
audiences Audiences
rootPool *x509.CertPool
}
// GetID returns the provisioner unique identifier. The name and credential id
// should uniquely identify any X5C provisioner.
func (p *X5C) GetID() string {
return "x5c/" + p.Name
}
// GetTokenID returns the identifier of the token.
func (p *X5C) GetTokenID(ott string) (string, error) {
// Validate payload
token, err := jose.ParseSigned(ott)
if err != nil {
return "", errors.Wrap(err, "error parsing token")
}
// Get claims w/out verification. We need to look up the provisioner
// key in order to verify the claims and we need the issuer from the claims
// before we can look up the provisioner.
var claims jose.Claims
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return "", errors.Wrap(err, "error verifying claims")
}
return claims.ID, nil
}
// GetName returns the name of the provisioner.
func (p *X5C) GetName() string {
return p.Name
}
// GetType returns the type of provisioner.
func (p *X5C) GetType() Type {
return TypeX5C
}
// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
func (p *X5C) GetEncryptedKey() (string, string, bool) {
return "", "", false
}
// Init initializes and validates the fields of a X5C type.
func (p *X5C) Init(config Config) error {
switch {
case p.Type == "":
return errors.New("provisioner type cannot be empty")
case p.Name == "":
return errors.New("provisioner name cannot be empty")
case len(p.Roots) == 0:
return errors.New("provisioner root(s) cannot be empty")
}
p.rootPool = x509.NewCertPool()
var (
block *pem.Block
rest = p.Roots
)
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return errors.Wrap(err, "error parsing x509 certificate from PEM block")
}
p.rootPool.AddCert(cert)
}
// Verify that at least one root was found.
if len(p.rootPool.Subjects()) == 0 {
return errors.Errorf("no x509 certificates found in roots attribute for provisioner %s", p.GetName())
}
// Update claims with global ones
var err error
if p.claimer, err = NewClaimer(p.Claims, config.Claims); err != nil {
return err
}
p.audiences = config.Audiences.WithFragment(p.GetID())
return nil
}
// authorizeToken performs common jwt authorization actions and returns the
// claims for case specific downstream parsing.
// e.g. a Sign request will auth/validate different fields than a Revoke request.
func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
}
verifiedChains, err := jwt.Headers[0].Certificates(x509.VerifyOptions{
Roots: p.rootPool,
})
if err != nil {
return nil, errors.Wrap(err, "error verifying x5c certificate chain")
}
leaf := verifiedChains[0][0]
if leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return nil, errors.New("certificate used to sign x5c token cannot be used for digital signature")
}
// Using the leaf certificates key to validate the claims accomplishes two
// things:
// 1. Asserts that the private key used to sign the token corresponds
// to the public certificate in the `x5c` header of the token.
// 2. Asserts that the claims are valid - have not been tampered with.
var claims x5cPayload
if err = jwt.Claims(leaf.PublicKey, &claims); err != nil {
return nil, errors.Wrap(err, "error parsing claims")
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
// more than a few minutes.
if err = claims.ValidateWithLeeway(jose.Expected{
Issuer: p.Name,
Time: time.Now().UTC(),
}, time.Minute); err != nil {
return nil, errors.Wrapf(err, "invalid token")
}
// validate audiences with the defaults
if !matchesAudience(claims.Audience, audiences) {
return nil, errors.New("invalid token: invalid audience claim (aud)")
}
if claims.Subject == "" {
return nil, errors.New("token subject cannot be empty")
}
// Save the verified chains on the x5c payload object.
claims.chains = verifiedChains
return &claims, nil
}
// AuthorizeRevoke returns an error if the provisioner does not have rights to
// revoke the certificate with serial number in the `sub` property.
func (p *X5C) AuthorizeRevoke(token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke)
return err
}
// AuthorizeSign validates the given token.
func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token, p.audiences.Sign)
if err != nil {
return nil, err
}
// Check for SSH sign-ing request.
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims)
}
// NOTE: This is for backwards compatibility with older versions of cli
// and certificates. Older versions added the token subject as the only SAN
// in a CSR by default.
if len(claims.SANs) == 0 {
claims.SANs = []string{claims.Subject}
}
dnsNames, ips, emails := x509util.SplitSANs(claims.SANs)
return []SignOption{
// modifiers / withOptions
newProvisionerExtensionOption(TypeX5C, p.Name, ""),
profileLimitDuration{p.claimer.DefaultTLSCertDuration(), claims.chains[0][0].NotAfter},
// validators
commonNameValidator(claims.Subject),
defaultPublicKeyValidator{},
dnsNamesValidator(dnsNames),
emailAddressesValidator(emails),
ipAddressesValidator(ips),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
}, nil
}
// AuthorizeRenewal returns an error if the renewal is disabled.
func (p *X5C) AuthorizeRenewal(cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
}
return nil
}
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *X5C) authorizeSSHSign(claims *x5cPayload) ([]SignOption, error) {
if claims.Step == nil || claims.Step.SSH == nil {
return nil, errors.New("authorization token must be an SSH provisioning token")
}
opts := claims.Step.SSH
signOptions := []SignOption{
// validates user's SSHOptions with the ones in the token
sshCertificateOptionsValidator(*opts),
// set the key id to the token subject
sshCertificateKeyIDModifier(claims.Subject),
}
// Add modifiers from custom claims
if opts.CertType != "" {
signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType))
}
if len(opts.Principals) > 0 {
signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals))
}
t := now()
if !opts.ValidAfter.IsZero() {
signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix()))
}
if !opts.ValidBefore.IsZero() {
signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix()))
}
// Default to a user certificate with no principals if not set
signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert})
return append(signOptions,
// Set the default extensions.
&sshDefaultExtensionModifier{},
// Checks the validity bounds, and set the validity if has not been set.
sshLimitValidityModifier(p.claimer, claims.chains[0][0].NotAfter),
// Validate public key.
&sshDefaultPublicKeyValidator{},
// Validate the validity period.
&sshCertificateValidityValidator{p.claimer},
// Require all the fields in the SSH certificate
&sshCertificateDefaultValidator{},
), nil
}

View file

@ -0,0 +1,751 @@
package provisioner
import (
"context"
"crypto/x509"
"net"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/jose"
)
func TestX5C_Getters(t *testing.T) {
p, err := generateX5C(nil)
assert.FatalError(t, err)
id := "x5c/" + p.Name
if got := p.GetID(); got != id {
t.Errorf("X5C.GetID() = %v, want %v:%v", got, p.Name, id)
}
if got := p.GetName(); got != p.Name {
t.Errorf("X5C.GetName() = %v, want %v", got, p.Name)
}
if got := p.GetType(); got != TypeX5C {
t.Errorf("X5C.GetType() = %v, want %v", got, TypeX5C)
}
kid, key, ok := p.GetEncryptedKey()
if kid != "" || key != "" || ok == true {
t.Errorf("X5C.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
kid, key, ok, "", "", false)
}
}
func TestX5C_Init(t *testing.T) {
type ProvisionerValidateTest struct {
p *X5C
err error
extraValid func(*X5C) error
}
tests := map[string]func(*testing.T) ProvisionerValidateTest{
"fail/empty": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &X5C{},
err: errors.New("provisioner type cannot be empty"),
}
},
"fail/empty-name": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &X5C{
Type: "X5C",
},
err: errors.New("provisioner name cannot be empty"),
}
},
"fail/empty-type": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &X5C{Name: "foo"},
err: errors.New("provisioner type cannot be empty"),
}
},
"fail/empty-key": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &X5C{Name: "foo", Type: "bar"},
err: errors.New("provisioner root(s) cannot be empty"),
}
},
"fail/no-valid-root-certs": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &X5C{Name: "foo", Type: "bar", Roots: []byte("foo"), audiences: testAudiences},
err: errors.Errorf("no x509 certificates found in roots attribute for provisioner foo"),
}
},
"fail/invalid-duration": func(t *testing.T) ProvisionerValidateTest {
p, err := generateX5C(nil)
assert.FatalError(t, err)
p.Claims = &Claims{DefaultTLSDur: &Duration{0}}
return ProvisionerValidateTest{
p: p,
err: errors.New("claims: DefaultTLSCertDuration must be greater than 0"),
}
},
"ok": func(t *testing.T) ProvisionerValidateTest {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return ProvisionerValidateTest{
p: p,
}
},
"ok/root-chain": func(t *testing.T) ProvisionerValidateTest {
p, err := generateX5C([]byte(`-----BEGIN CERTIFICATE-----
MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw
EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw
MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl
qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC
AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99
oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw
E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1
2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC
lgsqsR63is+0YQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES
MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz
OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB
BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO
BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T
AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD
VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH
1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY
M46l92gdOozT
-----END CERTIFICATE-----`))
assert.FatalError(t, err)
return ProvisionerValidateTest{
p: p,
extraValid: func(p *X5C) error {
numCerts := len(p.rootPool.Subjects())
if numCerts != 2 {
return errors.Errorf("unexpected number of certs: want 2, but got %d", numCerts)
}
return nil
},
}
},
}
config := Config{
Claims: globalProvisionerClaims,
Audiences: testAudiences,
}
for name, get := range tests {
t.Run(name, func(t *testing.T) {
tc := get(t)
err := tc.p.Init(config)
if err != nil {
if assert.NotNil(t, tc.err) {
assert.Equals(t, tc.err.Error(), err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, tc.p.audiences, config.Audiences.WithFragment(tc.p.GetID()))
if tc.extraValid != nil {
assert.Nil(t, tc.extraValid(tc.p))
}
}
}
})
}
}
func TestX5C_authorizeToken(t *testing.T) {
type test struct {
p *X5C
token string
err error
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
err: errors.New("error parsing token"),
}
},
"fail/invalid-cert-chain": func(t *testing.T) test {
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
MIIBpTCCAUugAwIBAgIRAOn2LHXjYyTXQ7PNjDTSKiIwCgYIKoZIzj0EAwIwHDEa
MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwOTE0MDk1NTM2WhcNMjkw
OTExMDk1NTM2WjAkMSIwIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENB
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2Cs0TY0dLM4b2s+z8+cc3JJp/W5H
zQRvICX/1aJ4MuObNLcvoSguJwJEkYpGB5fhb0KvoL+ebHfEOywGNwrWkaNmMGQw
DgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNLJ
4ZXoX9cI6YkGPxgs2US3ssVzMB8GA1UdIwQYMBaAFGIwpqz85wL29aF47Vj9XSVM
P9K7MAoGCCqGSM49BAMCA0gAMEUCIQC5c1ldDcesDb31GlO5cEJvOcRrIrNtkk8m
a5wpg+9s6QIgHIW6L60F8klQX+EO3o0SBqLeNcaskA4oSZsKjEdpSGo=
-----END CERTIFICATE-----`))
assert.FatalError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"),
}
},
"fail/doubled-up-self-signed-cert": func(t *testing.T) test {
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w
DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow
EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI
3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG
M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ
BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS
ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G
EXAHTA9L
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w
DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow
EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI
3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG
M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ
BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS
ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G
EXAHTA9L
-----END CERTIFICATE-----`))
assert.FatalError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"),
}
},
"fail/digital-signature-ext-required": func(t *testing.T) test {
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
MIIBuTCCAV+gAwIBAgIQeRJLdDMIdn/T2ORKxYABezAKBggqhkjOPQQDAjAcMRow
GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMjQxMTRaGA8yMTE5
MDkwODAyNDExMlowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEDA1nGTOujobkcBWklyvymhWE5gQlvNLarVzhhhvPDw+MK2LX
yqkXrYZM10GrwQZuQ7ykHnjz00U/KXpPRQ7+0qOBiDCBhTAOBgNVHQ8BAf8EBAMC
BSAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQYv0AK
3GUOvC+m8ZTfyhn7tKQOazAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp
czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIhAPmertx0
lchRU3kAu647exvlhEr1xosPOu6P8kVYbtTEAiAA51w9EYIT/Zb26M3eQV817T2g
Dnhl0ElPQsA92pkqbA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw
EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw
MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl
qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC
AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99
oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw
E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1
2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC
lgsqsR63is+0YQ==
-----END CERTIFICATE-----`))
assert.FatalError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("certificate used to sign x5c token cannot be used for digital signature"),
}
},
"fail/signature-does-not-match-x5c-pub-key": func(t *testing.T) test {
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
MIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow
GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5
MDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr
VpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC
BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW
f4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp
czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN
wtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z
AvUgkUQ2G25NBRmX
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw
EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw
MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl
qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC
AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99
oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw
E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1
2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC
lgsqsR63is+0YQ==
-----END CERTIFICATE-----`))
assert.FatalError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", "foobar", testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive"),
}
},
"fail/invalid-issuer": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", "foobar", testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"),
}
},
"fail/invalid-audience": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.GetName(), "foobar", "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("invalid token: invalid audience claim (aud)"),
}
},
"fail/empty-subject": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("token subject cannot be empty"),
}
},
"ok": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.NotNil(t, claims)
assert.NotNil(t, claims.chains)
}
}
})
}
}
func TestX5C_AuthorizeSign(t *testing.T) {
type test struct {
p *X5C
token string
ctx context.Context
err error
dns []string
emails []string
ips []net.IP
}
tests := map[string]func(*testing.T) test{
"fail/invalid-token": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
ctx: NewContextWithMethod(context.Background(), SignMethod),
err: errors.New("error parsing token"),
}
},
"fail/ssh/disabled": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
p.claimer.claims = provisionerClaims()
*p.claimer.claims.EnableSSHCA = false
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignSSHMethod),
token: tok,
err: errors.Errorf("ssh ca is disabled for provisioner x5c/%s", p.GetName()),
}
},
"fail/ssh/invalid-token": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignSSHMethod),
token: tok,
err: errors.New("authorization token must be an SSH provisioning token"),
}
},
"ok/empty-sans": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignMethod),
token: tok,
dns: []string{"foo"},
emails: []string{},
ips: []net.IP{},
}
},
"ok/multi-sans": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"127.0.0.1", "foo", "max@smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignMethod),
token: tok,
dns: []string{"foo"},
emails: []string{"max@smallstep.com"},
ips: []net.IP{net.ParseIP("127.0.0.1")},
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
tot := 0
for _, o := range opts {
switch v := o.(type) {
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeX5C))
assert.Equals(t, v.Name, tc.p.GetName())
assert.Equals(t, v.CredentialID, "")
assert.Len(t, 0, v.KeyValuePairs)
case profileLimitDuration:
assert.Equals(t, v.def, tc.p.claimer.DefaultTLSCertDuration())
claims, err := tc.p.authorizeToken(tc.token, tc.p.audiences.Sign)
assert.FatalError(t, err)
assert.Equals(t, v.notAfter, claims.chains[0][0].NotAfter)
case commonNameValidator:
assert.Equals(t, string(v), "foo")
case defaultPublicKeyValidator:
case dnsNamesValidator:
assert.Equals(t, []string(v), tc.dns)
case emailAddressesValidator:
assert.Equals(t, []string(v), tc.emails)
case ipAddressesValidator:
assert.Equals(t, []net.IP(v), tc.ips)
case *validityValidator:
assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration())
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
tot++
}
assert.Equals(t, tot, 8)
}
}
}
})
}
}
func TestX5C_authorizeSSHSign(t *testing.T) {
_, fn := mockNow()
defer fn()
type test struct {
p *X5C
claims *x5cPayload
err error
}
tests := map[string]func(*testing.T) test{
"fail/no-Step-claim": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
claims: new(x5cPayload),
err: errors.New("authorization token must be an SSH provisioning token"),
}
},
"fail/no-SSH-subattribute-in-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
claims: &x5cPayload{Step: new(stepPayload)},
err: errors.New("authorization token must be an SSH provisioning token"),
}
},
"ok/with-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
return test{
p: p,
claims: &x5cPayload{
Step: &stepPayload{SSH: &SSHOptions{
CertType: SSHHostCert,
Principals: []string{"max", "mariano", "alan"},
ValidAfter: TimeDuration{d: 5 * time.Minute},
ValidBefore: TimeDuration{d: 10 * time.Minute},
}},
Claims: jose.Claims{Subject: "foo"},
chains: [][]*x509.Certificate{certs},
},
}
},
"ok/without-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
return test{
p: p,
claims: &x5cPayload{
Step: &stepPayload{SSH: &SSHOptions{}},
Claims: jose.Claims{Subject: "foo"},
chains: [][]*x509.Certificate{certs},
},
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.authorizeSSHSign(tc.claims); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
tot := 0
nw := now()
for _, o := range opts {
switch v := o.(type) {
case sshCertificateOptionsValidator:
tc.claims.Step.SSH.ValidAfter.t = time.Time{}
tc.claims.Step.SSH.ValidBefore.t = time.Time{}
assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH)
case sshCertificateKeyIDModifier:
assert.Equals(t, string(v), "foo")
case sshCertificateCertTypeModifier:
assert.Equals(t, string(v), tc.claims.Step.SSH.CertType)
case sshCertificatePrincipalsModifier:
assert.Equals(t, []string(v), tc.claims.Step.SSH.Principals)
case sshCertificateValidAfterModifier:
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix())
case sshCertificateValidBeforeModifier:
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix())
case sshCertificateDefaultsModifier:
assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert})
case *sshValidityModifier:
assert.Equals(t, v.Claimer, tc.p.claimer)
assert.Equals(t, v.validBefore, tc.claims.chains[0][0].NotAfter)
case *sshCertificateValidityValidator:
assert.Equals(t, v.Claimer, tc.p.claimer)
case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator,
*sshCertificateDefaultValidator:
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
tot++
}
if len(tc.claims.Step.SSH.CertType) > 0 {
assert.Equals(t, tot, 12)
} else {
assert.Equals(t, tot, 8)
}
}
}
}
})
}
}
func TestX5C_AuthorizeRevoke(t *testing.T) {
type test struct {
p *X5C
token string
err error
}
tests := map[string]func(*testing.T) test{
"fail/invalid-token": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
err: errors.New("error parsing token"),
}
},
"ok": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Revoke[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeRevoke(tc.token); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func TestX5C_AuthorizeRenewal(t *testing.T) {
p1, err := generateX5C(nil)
assert.FatalError(t, err)
p2, err := generateX5C(nil)
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
prov *X5C
args args
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("X5C.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -208,14 +208,15 @@ func TestSign(t *testing.T) {
},
"fail rsa key too short": func(t *testing.T) *signTest {
shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST-----
MIIBdDCB2wIBADAOMQwwCgYDVQQDEwNmb28wgaIwDQYJKoZIhvcNAQEBBQADgZAA
MIGMAoGEAK8dks7oV6kcIFEaWna7CDGYPAE8IL7rNi+ruQ1dIYz+JtxT7OPjbCn/
t5iqni96+35iS/8CvMtEuquOMTMSWOWwlurrbTbLqCazuz/g233o8udxSxhny3cY
wHogp4cXCX6cFll6DeUnoCEuTTSIu8IBHbK48VfNw4V4gGz6cp/H93HrAgMBAAGg
ITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQHMAWCA2ZvbzANBgkqhkiG9w0BAQsF
AAOBhABCZsYM+Kgje68Z9Fjl2+cBwtQHvZDarh+cz6W1SchinZ1T0aNQvSj/otOe
ttnEF4Rq8zqzr4fbv+AF451Mx36AkfgZr9XWGzxidrH+fBCNWXWNR+ymhrL6UFTG
2FbarLt9jN2aJLAYQPwtSeGTAZ74tLOPRPnTP6aMfFNg4XCR0uveHA==
MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG
9w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4
k205rv5/YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ
7JMMNB43CSLB2oNuwJjLeAM/yBzlhRnvpKjrNSfSV+cH54FXdnbFbcTFMStnjqKG
MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw
IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ//cXMFK1SP8ktRU
N2++E8Ww0Tet9oyNRArqxxS/UyVio63D3wynzRAB25PFGpYG1cN4b81Gv/foFUT6
W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr
ZYtQ9Ot36qc=
-----END CERTIFICATE REQUEST-----`
block, _ := pem.Decode([]byte(shortRSAKeyPEM))
assert.FatalError(t, err)

View file

@ -178,7 +178,7 @@ func (r *TLSRenewer) renewCertificate() {
}
func (r *TLSRenewer) nextRenewDuration(notAfter time.Time) time.Duration {
d := notAfter.Sub(time.Now()) - r.renewBefore
d := time.Until(notAfter) - r.renewBefore
n := rand.Int63n(int64(r.renewJitter))
d -= time.Duration(n)
if d < 0 {

View file

@ -28,20 +28,17 @@ func StopHandler(servers ...Stopper) {
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(signals)
for {
select {
case sig := <-signals:
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
log.Println("shutting down ...")
for _, server := range servers {
err := server.Stop()
if err != nil {
log.Printf("error stopping server: %s", err.Error())
}
for sig := range signals {
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
log.Println("shutting down ...")
for _, server := range servers {
err := server.Stop()
if err != nil {
log.Printf("error stopping server: %s", err.Error())
}
return
}
return
}
}
}
@ -54,28 +51,25 @@ func StopReloaderHandler(servers ...StopReloader) {
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer signal.Stop(signals)
for {
select {
case sig := <-signals:
switch sig {
case syscall.SIGHUP:
log.Println("reloading ...")
for _, server := range servers {
err := server.Reload()
if err != nil {
log.Printf("error reloading server: %+v", err)
}
for sig := range signals {
switch sig {
case syscall.SIGHUP:
log.Println("reloading ...")
for _, server := range servers {
err := server.Reload()
if err != nil {
log.Printf("error reloading server: %+v", err)
}
case syscall.SIGINT, syscall.SIGTERM:
log.Println("shutting down ...")
for _, server := range servers {
err := server.Stop()
if err != nil {
log.Printf("error stopping server: %s", err.Error())
}
}
return
}
case syscall.SIGINT, syscall.SIGTERM:
log.Println("shutting down ...")
for _, server := range servers {
err := server.Stop()
if err != nil {
log.Printf("error stopping server: %s", err.Error())
}
}
return
}
}
}

View file

@ -553,7 +553,7 @@ func equalPools(a, b *x509.CertPool) bool {
for i := range subjects {
sB[i] = string(subjects[i])
}
sort.Sort(sort.StringSlice(sA))
sort.Sort(sort.StringSlice(sB))
sort.Strings(sA)
sort.Strings(sB)
return reflect.DeepEqual(sA, sB)
}

View file

@ -32,7 +32,7 @@ func (l *LoggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t := time.Now()
rw := NewResponseLogger(w)
l.next.ServeHTTP(rw, r)
d := time.Now().Sub(t)
d := time.Since(t)
l.writeEntry(rw, r, t, d)
}