forked from TrueCloudLab/certificates
Add x5c provisioner capabilities
This commit is contained in:
parent
2781045524
commit
d368791606
32 changed files with 1805 additions and 246 deletions
|
@ -51,6 +51,7 @@ linters:
|
|||
- deadcode
|
||||
- staticcheck
|
||||
- unused
|
||||
- gosimple
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
24
authority/provisioner/testdata/x5c-leaf.crt
vendored
Normal file
24
authority/provisioner/testdata/x5c-leaf.crt
vendored
Normal 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-----
|
5
authority/provisioner/testdata/x5c-leaf.key
vendored
Normal file
5
authority/provisioner/testdata/x5c-leaf.key
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIALytC4LyTTAagMLMv+rzq2vtfhFkhuyBz4kqsnRs6zioAoGCCqGSM49
|
||||
AwEHoUQDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvrVpgS
|
||||
PXYruNRFduPWX564Abz/TDmb276JbKGeQg==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -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
|
||||
}
|
||||
|
|
267
authority/provisioner/x5c.go
Normal file
267
authority/provisioner/x5c.go
Normal 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
|
||||
}
|
751
authority/provisioner/x5c_test.go
Normal file
751
authority/provisioner/x5c_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
58
ca/signal.go
58
ca/signal.go
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue