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
|
- deadcode
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- unused
|
- unused
|
||||||
|
- gosimple
|
||||||
|
|
||||||
run:
|
run:
|
||||||
skip-dirs:
|
skip-dirs:
|
||||||
|
|
|
@ -23,9 +23,6 @@
|
||||||
# non-go = false
|
# non-go = false
|
||||||
# go-tests = true
|
# go-tests = true
|
||||||
# unused-packages = true
|
# unused-packages = true
|
||||||
[[override]]
|
|
||||||
name = "gopkg.in/alecthomas/kingpin.v3-unstable"
|
|
||||||
revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "master"
|
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,
|
w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink,
|
||||||
acme.URLSafeProvisionerName(prov), true, acc.GetID()))
|
acme.URLSafeProvisionerName(prov), true, acc.GetID()))
|
||||||
api.JSONStatus(w, acc, httpStatus)
|
api.JSONStatus(w, acc, httpStatus)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUpdateAccount is the api for updating an ACME account.
|
// 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()))
|
w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink, acme.URLSafeProvisionerName(prov), true, acc.GetID()))
|
||||||
api.JSON(w, acc)
|
api.JSON(w, acc)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func logOrdersByAccount(w http.ResponseWriter, oids []string) {
|
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)
|
api.JSON(w, orders)
|
||||||
logOrdersByAccount(w, orders)
|
logOrdersByAccount(w, orders)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,6 @@ func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDirectory is the ACME resource for returning a directory configuration
|
// 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)
|
dir := h.Auth.GetDirectory(prov)
|
||||||
api.JSON(w, dir)
|
api.JSON(w, dir)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthz ACME api for retrieving an Authz.
|
// 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()))
|
w.Header().Set("Location", h.Auth.GetLink(acme.AuthzLink, acme.URLSafeProvisionerName(prov), true, authz.GetID()))
|
||||||
api.JSON(w, authz)
|
api.JSON(w, authz)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChallenge ACME api for retrieving a Challenge.
|
// 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().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()))
|
w.Header().Set("Location", getLink(acme.ChallengeLink, acme.URLSafeProvisionerName(prov), true, ch.GetID()))
|
||||||
api.JSON(w, ch)
|
api.JSON(w, ch)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCertificate ACME api for retrieving a Certificate.
|
// 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.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8")
|
||||||
w.Write(certBytes)
|
w.Write(certBytes)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,6 @@ func (h *Handler) addNonce(next nextHTTP) nextHTTP {
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
logNonce(w, nonce)
|
logNonce(w, nonce)
|
||||||
next(w, r)
|
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"))
|
w.Header().Add("Link", link(h.Auth.GetLink(acme.DirectoryLink, acme.URLSafeProvisionerName(prov), true), "index"))
|
||||||
next(w, r)
|
next(w, r)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +85,6 @@ func (h *Handler) verifyContentType(next nextHTTP) nextHTTP {
|
||||||
}
|
}
|
||||||
api.WriteError(w, acme.MalformedErr(errors.Errorf(
|
api.WriteError(w, acme.MalformedErr(errors.Errorf(
|
||||||
"expected content-type to be in %s, but got %s", expected, ct)))
|
"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)
|
ctx := context.WithValue(r.Context(), jwsContextKey, jws)
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +198,6 @@ func (h *Handler) validateJWS(next nextHTTP) nextHTTP {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next(w, r)
|
next(w, r)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +243,6 @@ func (h *Handler) extractJWK(next nextHTTP) nextHTTP {
|
||||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||||
}
|
}
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,7 +269,6 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP {
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, provisionerContextKey, p)
|
ctx = context.WithValue(ctx, provisionerContextKey, p)
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,7 +348,6 @@ func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {
|
||||||
isEmptyJSON: string(payload) == "{}",
|
isEmptyJSON: string(payload) == "{}",
|
||||||
})
|
})
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,6 +364,5 @@ func (h *Handler) isPostAsGet(next nextHTTP) nextHTTP {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next(w, r)
|
next(w, r)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ var testBody = []byte("foo")
|
||||||
|
|
||||||
func testNext(w http.ResponseWriter, r *http.Request) {
|
func testNext(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerAddNonce(t *testing.T) {
|
func TestHandlerAddNonce(t *testing.T) {
|
||||||
|
@ -471,7 +470,6 @@ func TestHandlerParseJWS(t *testing.T) {
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
assert.Equals(t, gotRaw, expRaw)
|
assert.Equals(t, gotRaw, expRaw)
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
},
|
},
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
}
|
}
|
||||||
|
@ -923,7 +921,6 @@ func TestHandlerLookupJWK(t *testing.T) {
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
assert.Equals(t, _jwk, jwk)
|
assert.Equals(t, _jwk, jwk)
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
},
|
},
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
}
|
}
|
||||||
|
@ -1114,7 +1111,6 @@ func TestHandlerExtractJWK(t *testing.T) {
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
assert.Equals(t, _jwk.KeyID, pub.KeyID)
|
assert.Equals(t, _jwk.KeyID, pub.KeyID)
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
},
|
},
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
}
|
}
|
||||||
|
@ -1139,7 +1135,6 @@ func TestHandlerExtractJWK(t *testing.T) {
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
assert.Equals(t, _jwk.KeyID, pub.KeyID)
|
assert.Equals(t, _jwk.KeyID, pub.KeyID)
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
},
|
},
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
}
|
}
|
||||||
|
@ -1448,7 +1443,6 @@ func TestHandlerValidateJWS(t *testing.T) {
|
||||||
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
|
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
|
||||||
next: func(w http.ResponseWriter, r *http.Request) {
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
},
|
},
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
}
|
}
|
||||||
|
@ -1479,7 +1473,6 @@ func TestHandlerValidateJWS(t *testing.T) {
|
||||||
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
|
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
|
||||||
next: func(w http.ResponseWriter, r *http.Request) {
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
},
|
},
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
}
|
}
|
||||||
|
@ -1510,7 +1503,6 @@ func TestHandlerValidateJWS(t *testing.T) {
|
||||||
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
|
ctx: context.WithValue(context.Background(), jwsContextKey, jws),
|
||||||
next: func(w http.ResponseWriter, r *http.Request) {
|
next: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(testBody)
|
w.Write(testBody)
|
||||||
return
|
|
||||||
},
|
},
|
||||||
statusCode: 200,
|
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()))
|
w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID()))
|
||||||
api.JSONStatus(w, o, http.StatusCreated)
|
api.JSONStatus(w, o, http.StatusCreated)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrder ACME api for retrieving an order.
|
// 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()))
|
w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID()))
|
||||||
api.JSON(w, o)
|
api.JSON(w, o)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FinalizeOrder attemptst to finalize an order and create a certificate.
|
// 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))
|
w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.ID))
|
||||||
api.JSON(w, o)
|
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 nil, errors.Errorf("unexpected method type %d in context", m)
|
||||||
}
|
}
|
||||||
return []SignOption{
|
return []SignOption{
|
||||||
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
// modifiers / withOptions
|
||||||
newProvisionerExtensionOption(TypeACME, p.Name, ""),
|
newProvisionerExtensionOption(TypeACME, p.Name, ""),
|
||||||
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
||||||
|
// validators
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
|
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -155,28 +155,23 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
||||||
if assert.NotNil(t, got) {
|
if assert.NotNil(t, got) {
|
||||||
assert.Len(t, 4, got)
|
assert.Len(t, 4, got)
|
||||||
|
|
||||||
_pdd := got[0]
|
for _, o := range got {
|
||||||
pdd, ok := _pdd.(profileDefaultDuration)
|
switch v := o.(type) {
|
||||||
assert.True(t, ok)
|
case *provisionerExtensionOption:
|
||||||
assert.Equals(t, pdd, profileDefaultDuration(86400000000000))
|
assert.Equals(t, v.Type, int(TypeACME))
|
||||||
|
assert.Equals(t, v.Name, tt.prov.GetName())
|
||||||
_peo := got[1]
|
assert.Equals(t, v.CredentialID, "")
|
||||||
peo, ok := _peo.(*provisionerExtensionOption)
|
assert.Len(t, 0, v.KeyValuePairs)
|
||||||
assert.True(t, ok)
|
case profileDefaultDuration:
|
||||||
assert.Equals(t, peo.Type, 6)
|
assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration())
|
||||||
assert.Equals(t, peo.Name, "test@acme-provisioner.com")
|
case defaultPublicKeyValidator:
|
||||||
assert.Equals(t, peo.CredentialID, "")
|
case *validityValidator:
|
||||||
assert.Equals(t, peo.KeyValuePairs, nil)
|
assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration())
|
||||||
|
assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration())
|
||||||
_vv := got[2]
|
default:
|
||||||
vv, ok := _vv.(*validityValidator)
|
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
// Check for the sign ssh method, default to sign X.509
|
||||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
if MethodFromContext(ctx) == SignSSHMethod {
|
||||||
if p.claimer.IsSSHCAEnabled() == false {
|
if !p.claimer.IsSSHCAEnabled() {
|
||||||
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
||||||
}
|
}
|
||||||
return p.authorizeSSHSign(payload)
|
return p.authorizeSSHSign(payload)
|
||||||
|
@ -296,10 +296,12 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(so,
|
return append(so,
|
||||||
|
// modifiers / withOptions
|
||||||
|
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
|
||||||
|
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
||||||
|
// validators
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
commonNameValidator(payload.Claims.Subject),
|
commonNameValidator(payload.Claims.Subject),
|
||||||
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
|
||||||
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
|
|
||||||
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
@ -466,13 +468,15 @@ func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) {
|
||||||
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
|
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
|
||||||
|
|
||||||
return append(signOptions,
|
return append(signOptions,
|
||||||
// set the default extensions
|
// Set the default extensions.
|
||||||
&sshDefaultExtensionModifier{},
|
&sshDefaultExtensionModifier{},
|
||||||
// checks the validity bounds, and set the validity if has not been set
|
// Set the validity bounds if not set.
|
||||||
&sshCertificateValidityModifier{p.claimer},
|
sshDefaultValidityModifier(p.claimer),
|
||||||
// validate public key
|
// Validate public key
|
||||||
&sshDefaultPublicKeyValidator{},
|
&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{},
|
&sshCertificateDefaultValidator{},
|
||||||
), nil
|
), 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
|
// Check for the sign ssh method, default to sign X.509
|
||||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
if MethodFromContext(ctx) == SignSSHMethod {
|
||||||
if p.claimer.IsSSHCAEnabled() == false {
|
if !p.claimer.IsSSHCAEnabled() {
|
||||||
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
||||||
}
|
}
|
||||||
return p.authorizeSSHSign(claims, name)
|
return p.authorizeSSHSign(claims, name)
|
||||||
|
@ -284,9 +284,11 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(so,
|
return append(so,
|
||||||
defaultPublicKeyValidator{},
|
// modifiers / withOptions
|
||||||
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
|
||||||
newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID),
|
newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID),
|
||||||
|
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
||||||
|
// validators
|
||||||
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
@ -323,13 +325,15 @@ func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption
|
||||||
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
|
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
|
||||||
|
|
||||||
return append(signOptions,
|
return append(signOptions,
|
||||||
// set the default extensions
|
// Set the default extensions.
|
||||||
&sshDefaultExtensionModifier{},
|
&sshDefaultExtensionModifier{},
|
||||||
// checks the validity bounds, and set the validity if has not been set
|
// Set the validity bounds if not set.
|
||||||
&sshCertificateValidityModifier{p.claimer},
|
sshDefaultValidityModifier(p.claimer),
|
||||||
// validate public key
|
// Validate public key
|
||||||
&sshDefaultPublicKeyValidator{},
|
&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{},
|
&sshCertificateDefaultValidator{},
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,6 +129,8 @@ func (c *Collection) LoadByCertificate(cert *x509.Certificate) (Interface, bool)
|
||||||
return c.Load("gcp/" + string(provisioner.Name))
|
return c.Load("gcp/" + string(provisioner.Name))
|
||||||
case TypeACME:
|
case TypeACME:
|
||||||
return c.Load("acme/" + string(provisioner.Name))
|
return c.Load("acme/" + string(provisioner.Name))
|
||||||
|
case TypeX5C:
|
||||||
|
return c.Load("x5c/" + string(provisioner.Name))
|
||||||
default:
|
default:
|
||||||
return c.Load(string(provisioner.CredentialID))
|
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
|
// Check for the sign ssh method, default to sign X.509
|
||||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
if MethodFromContext(ctx) == SignSSHMethod {
|
||||||
if p.claimer.IsSSHCAEnabled() == false {
|
if !p.claimer.IsSSHCAEnabled() {
|
||||||
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
||||||
}
|
}
|
||||||
return p.authorizeSSHSign(claims)
|
return p.authorizeSSHSign(claims)
|
||||||
|
@ -237,9 +237,11 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(so,
|
return append(so,
|
||||||
defaultPublicKeyValidator{},
|
// modifiers / withOptions
|
||||||
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
|
||||||
newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName),
|
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()),
|
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
@ -378,13 +380,15 @@ func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) {
|
||||||
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
|
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
|
||||||
|
|
||||||
return append(signOptions,
|
return append(signOptions,
|
||||||
// set the default extensions
|
// Set the default extensions
|
||||||
&sshDefaultExtensionModifier{},
|
&sshDefaultExtensionModifier{},
|
||||||
// checks the validity bounds, and set the validity if has not been set
|
// Set the validity bounds if not set.
|
||||||
&sshCertificateValidityModifier{p.claimer},
|
sshDefaultValidityModifier(p.claimer),
|
||||||
// validate public key
|
// Validate public key
|
||||||
&sshDefaultPublicKeyValidator{},
|
&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{},
|
&sshCertificateDefaultValidator{},
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,9 +141,9 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for SSH token
|
// Check for SSH sign-ing request.
|
||||||
if claims.Step != nil && claims.Step.SSH != nil {
|
if MethodFromContext(ctx) == SignSSHMethod {
|
||||||
if p.claimer.IsSSHCAEnabled() == false {
|
if !p.claimer.IsSSHCAEnabled() {
|
||||||
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
|
||||||
}
|
}
|
||||||
return p.authorizeSSHSign(claims)
|
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)
|
dnsNames, ips, emails := x509util.SplitSANs(claims.SANs)
|
||||||
return []SignOption{
|
return []SignOption{
|
||||||
defaultPublicKeyValidator{},
|
// modifiers / withOptions
|
||||||
commonNameValidator(claims.Subject),
|
|
||||||
dnsNamesValidator(dnsNames),
|
|
||||||
ipAddressesValidator(ips),
|
|
||||||
emailAddressesValidator(emails),
|
|
||||||
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
|
|
||||||
newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID),
|
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()),
|
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -180,6 +182,9 @@ func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||||
func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
|
func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
|
||||||
t := now()
|
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
|
opts := claims.Step.SSH
|
||||||
signOptions := []SignOption{
|
signOptions := []SignOption{
|
||||||
// validates user's SSHOptions with the ones in the token
|
// 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})
|
signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert})
|
||||||
|
|
||||||
return append(signOptions,
|
return append(signOptions,
|
||||||
// set the default extensions
|
// Set the default extensions.
|
||||||
&sshDefaultExtensionModifier{},
|
&sshDefaultExtensionModifier{},
|
||||||
// checks the validity bounds, and set the validity if has not been set
|
// Set the validity bounds if not set.
|
||||||
&sshCertificateValidityModifier{p.claimer},
|
sshDefaultValidityModifier(p.claimer),
|
||||||
// validate public key
|
// Validate public key
|
||||||
&sshDefaultPublicKeyValidator{},
|
&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{},
|
&sshCertificateDefaultValidator{},
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,33 +6,16 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/cli/jose"
|
"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) {
|
func TestJWK_Getters(t *testing.T) {
|
||||||
p, err := generateJWK()
|
p, err := generateJWK()
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -247,7 +230,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
||||||
key1, err := decryptJSONWebKey(p1.EncryptedKey)
|
key1, err := decryptJSONWebKey(p1.EncryptedKey)
|
||||||
assert.FatalError(t, err)
|
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)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
t2, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{}, time.Now(), key1)
|
t2, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{}, time.Now(), key1)
|
||||||
|
@ -264,10 +247,13 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
||||||
prov *JWK
|
prov *JWK
|
||||||
args args
|
args args
|
||||||
err error
|
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")},
|
{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},
|
{"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},
|
{"ok-no-sans", p1, args{t2}, nil, []string{"subject"}, []string{}, []net.IP{}},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -279,19 +265,30 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
||||||
} else {
|
} else {
|
||||||
if assert.NotNil(t, got) {
|
if assert.NotNil(t, got) {
|
||||||
assert.Len(t, 8, got)
|
assert.Len(t, 8, got)
|
||||||
|
for _, o := range got {
|
||||||
_cnv := got[1]
|
switch v := o.(type) {
|
||||||
cnv, ok := _cnv.(commonNameValidator)
|
case *provisionerExtensionOption:
|
||||||
assert.True(t, ok)
|
assert.Equals(t, v.Type, int(TypeJWK))
|
||||||
assert.Equals(t, string(cnv), "subject")
|
assert.Equals(t, v.Name, tt.prov.GetName())
|
||||||
|
assert.Equals(t, v.CredentialID, tt.prov.Key.KeyID)
|
||||||
_dnv := got[2]
|
assert.Len(t, 0, v.KeyValuePairs)
|
||||||
dnv, ok := _dnv.(dnsNamesValidator)
|
case profileDefaultDuration:
|
||||||
assert.True(t, ok)
|
assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration())
|
||||||
if tt.name == "ok-sans" {
|
case commonNameValidator:
|
||||||
assert.Equals(t, []string(dnv), []string{"test.smallstep.com"})
|
assert.Equals(t, string(v), "subject")
|
||||||
} else {
|
case defaultPublicKeyValidator:
|
||||||
assert.Equals(t, []string(dnv), []string{"subject"})
|
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
|
// Check for the sign ssh method, default to sign X.509
|
||||||
if m := MethodFromContext(ctx); m == SignSSHMethod {
|
if MethodFromContext(ctx) == SignSSHMethod {
|
||||||
if o.claimer.IsSSHCAEnabled() == false {
|
if !o.claimer.IsSSHCAEnabled() {
|
||||||
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID())
|
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID())
|
||||||
}
|
}
|
||||||
return o.authorizeSSHSign(claims)
|
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{
|
so := []SignOption{
|
||||||
defaultPublicKeyValidator{},
|
// modifiers / withOptions
|
||||||
profileDefaultDuration(o.claimer.DefaultTLSCertDuration()),
|
|
||||||
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
|
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
|
||||||
|
profileDefaultDuration(o.claimer.DefaultTLSCertDuration()),
|
||||||
|
// validators
|
||||||
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()),
|
newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()),
|
||||||
}
|
}
|
||||||
// Admins should be able to authorize any SAN
|
// 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))
|
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
|
||||||
|
|
||||||
return append(signOptions,
|
return append(signOptions,
|
||||||
// set the default extensions
|
// Set the default extensions
|
||||||
&sshDefaultExtensionModifier{},
|
&sshDefaultExtensionModifier{},
|
||||||
// checks the validity bounds, and set the validity if has not been set
|
// Set the validity bounds if not set.
|
||||||
&sshCertificateValidityModifier{o.claimer},
|
sshDefaultValidityModifier(o.claimer),
|
||||||
// validate public key
|
// Validate public key
|
||||||
&sshDefaultPublicKeyValidator{},
|
&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{},
|
&sshCertificateDefaultValidator{},
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/cli/jose"
|
||||||
)
|
)
|
||||||
|
@ -298,12 +299,32 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else {
|
} else {
|
||||||
assert.NotNil(t, got)
|
if assert.NotNil(t, got) {
|
||||||
if tt.name == "admin" {
|
if tt.name == "admin" {
|
||||||
assert.Len(t, 3, got)
|
assert.Len(t, 4, got)
|
||||||
} else {
|
} else {
|
||||||
assert.Len(t, 5, got)
|
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
|
TypeAzure Type = 5
|
||||||
// TypeACME is used to indicate the ACME provisioners.
|
// TypeACME is used to indicate the ACME provisioners.
|
||||||
TypeACME Type = 6
|
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 is the key for the 'revoke' audiences in the audiences map.
|
||||||
RevokeAudienceKey = "revoke"
|
RevokeAudienceKey = "revoke"
|
||||||
|
@ -108,6 +110,8 @@ func (t Type) String() string {
|
||||||
return "Azure"
|
return "Azure"
|
||||||
case TypeACME:
|
case TypeACME:
|
||||||
return "ACME"
|
return "ACME"
|
||||||
|
case TypeX5C:
|
||||||
|
return "X5C"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -157,6 +161,8 @@ func (l *List) UnmarshalJSON(data []byte) error {
|
||||||
p = &Azure{}
|
p = &Azure{}
|
||||||
case "acme":
|
case "acme":
|
||||||
p = &ACME{}
|
p = &ACME{}
|
||||||
|
case "x5c":
|
||||||
|
p = &X5C{}
|
||||||
default:
|
default:
|
||||||
// Skip unsupported provisioners. A client using this method may be
|
// Skip unsupported provisioners. A client using this method may be
|
||||||
// compiled with a version of smallstep/certificates that does not
|
// 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)
|
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
|
// emailOnlyIdentity is a CertificateRequestValidator that checks that the only
|
||||||
// SAN provided is the given email address.
|
// SAN provided is the given email address.
|
||||||
type emailOnlyIdentity string
|
type emailOnlyIdentity string
|
||||||
|
@ -197,7 +184,61 @@ func (v emailAddressesValidator) Valid(req *x509.CertificateRequest) error {
|
||||||
return nil
|
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 {
|
type validityValidator struct {
|
||||||
min time.Duration
|
min time.Duration
|
||||||
max time.Duration
|
max time.Duration
|
||||||
|
@ -208,7 +249,8 @@ func newValidityValidator(min, max time.Duration) *validityValidator {
|
||||||
return &validityValidator{min: min, max: max}
|
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 {
|
func (v *validityValidator) Valid(crt *x509.Certificate) error {
|
||||||
var (
|
var (
|
||||||
na = crt.NotAfter
|
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
|
// validity bounds, setting them if they are not provided. It will fail if a
|
||||||
// CertType has not been set or is not valid.
|
// CertType has not been set or is not valid.
|
||||||
type sshCertificateValidityModifier struct {
|
type sshValidityModifier struct {
|
||||||
*Claimer
|
*Claimer
|
||||||
|
validBefore time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *sshCertificateValidityModifier) Modify(cert *ssh.Certificate) error {
|
func (m *sshValidityModifier) Modify(cert *ssh.Certificate) error {
|
||||||
var d, min, max time.Duration
|
var d time.Duration
|
||||||
|
|
||||||
switch cert.CertType {
|
switch cert.CertType {
|
||||||
case ssh.UserCert:
|
case ssh.UserCert:
|
||||||
d = m.DefaultUserSSHCertDuration()
|
d = m.DefaultUserSSHCertDuration()
|
||||||
min = m.MinUserSSHCertDuration()
|
|
||||||
max = m.MaxUserSSHCertDuration()
|
|
||||||
case ssh.HostCert:
|
case ssh.HostCert:
|
||||||
d = m.DefaultHostSSHCertDuration()
|
d = m.DefaultHostSSHCertDuration()
|
||||||
min = m.MinHostSSHCertDuration()
|
|
||||||
max = m.MaxHostSSHCertDuration()
|
|
||||||
case 0:
|
case 0:
|
||||||
return errors.New("ssh certificate type has not been set")
|
return errors.New("ssh certificate type has not been set")
|
||||||
default:
|
default:
|
||||||
return errors.Errorf("unknown ssh certificate type %d", cert.CertType)
|
return errors.Errorf("unknown ssh certificate type %d", cert.CertType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasLimit := !m.validBefore.IsZero()
|
||||||
|
|
||||||
|
n := now()
|
||||||
if cert.ValidAfter == 0 {
|
if cert.ValidAfter == 0 {
|
||||||
cert.ValidAfter = uint64(now().Truncate(time.Second).Unix())
|
cert.ValidAfter = uint64(n.Truncate(time.Second).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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
if cert.ValidBefore == 0 {
|
|
||||||
t := time.Unix(int64(cert.ValidAfter), 0)
|
|
||||||
cert.ValidBefore = uint64(t.Add(d).Unix())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
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
|
// sshCertificateOptionsValidator validates the user SSHOptions with the ones
|
||||||
|
@ -245,6 +265,48 @@ func (v sshCertificateOptionsValidator) Valid(got SSHOptions) error {
|
||||||
return want.match(got)
|
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
|
// sshCertificateDefaultValidator implements a simple validator for all the
|
||||||
// fields in the SSH certificate.
|
// fields in the SSH certificate.
|
||||||
type sshCertificateDefaultValidator struct{}
|
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,10 +19,45 @@ import (
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/cli/jose"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testAudiences = Audiences{
|
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"},
|
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"},
|
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-----
|
const awsTestCertificate = `-----BEGIN CERTIFICATE-----
|
||||||
MIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw
|
MIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw
|
||||||
|
@ -162,6 +197,58 @@ func generateJWK() (*JWK, error) {
|
||||||
}, nil
|
}, 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) {
|
func generateOIDC() (*OIDC, error) {
|
||||||
name, err := randutil.Alphanumeric(10)
|
name, err := randutil.Alphanumeric(10)
|
||||||
if err != nil {
|
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)
|
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) {
|
type tokOption func(*jose.SignerOptions) error
|
||||||
sig, err := jose.NewSigner(
|
|
||||||
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
func withX5CHdr(certs []*x509.Certificate) tokOption {
|
||||||
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -742,3 +849,24 @@ func generateACME() (*ACME, error) {
|
||||||
}
|
}
|
||||||
return p, nil
|
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 {
|
"fail rsa key too short": func(t *testing.T) *signTest {
|
||||||
shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST-----
|
shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST-----
|
||||||
MIIBdDCB2wIBADAOMQwwCgYDVQQDEwNmb28wgaIwDQYJKoZIhvcNAQEBBQADgZAA
|
MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG
|
||||||
MIGMAoGEAK8dks7oV6kcIFEaWna7CDGYPAE8IL7rNi+ruQ1dIYz+JtxT7OPjbCn/
|
9w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4
|
||||||
t5iqni96+35iS/8CvMtEuquOMTMSWOWwlurrbTbLqCazuz/g233o8udxSxhny3cY
|
k205rv5/YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ
|
||||||
wHogp4cXCX6cFll6DeUnoCEuTTSIu8IBHbK48VfNw4V4gGz6cp/H93HrAgMBAAGg
|
7JMMNB43CSLB2oNuwJjLeAM/yBzlhRnvpKjrNSfSV+cH54FXdnbFbcTFMStnjqKG
|
||||||
ITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQHMAWCA2ZvbzANBgkqhkiG9w0BAQsF
|
MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw
|
||||||
AAOBhABCZsYM+Kgje68Z9Fjl2+cBwtQHvZDarh+cz6W1SchinZ1T0aNQvSj/otOe
|
IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ//cXMFK1SP8ktRU
|
||||||
ttnEF4Rq8zqzr4fbv+AF451Mx36AkfgZr9XWGzxidrH+fBCNWXWNR+ymhrL6UFTG
|
N2++E8Ww0Tet9oyNRArqxxS/UyVio63D3wynzRAB25PFGpYG1cN4b81Gv/foFUT6
|
||||||
2FbarLt9jN2aJLAYQPwtSeGTAZ74tLOPRPnTP6aMfFNg4XCR0uveHA==
|
W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr
|
||||||
|
ZYtQ9Ot36qc=
|
||||||
-----END CERTIFICATE REQUEST-----`
|
-----END CERTIFICATE REQUEST-----`
|
||||||
block, _ := pem.Decode([]byte(shortRSAKeyPEM))
|
block, _ := pem.Decode([]byte(shortRSAKeyPEM))
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
|
@ -178,7 +178,7 @@ func (r *TLSRenewer) renewCertificate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *TLSRenewer) nextRenewDuration(notAfter time.Time) time.Duration {
|
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))
|
n := rand.Int63n(int64(r.renewJitter))
|
||||||
d -= time.Duration(n)
|
d -= time.Duration(n)
|
||||||
if d < 0 {
|
if d < 0 {
|
||||||
|
|
10
ca/signal.go
10
ca/signal.go
|
@ -28,9 +28,7 @@ func StopHandler(servers ...Stopper) {
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer signal.Stop(signals)
|
defer signal.Stop(signals)
|
||||||
|
|
||||||
for {
|
for sig := range signals {
|
||||||
select {
|
|
||||||
case sig := <-signals:
|
|
||||||
switch sig {
|
switch sig {
|
||||||
case syscall.SIGINT, syscall.SIGTERM:
|
case syscall.SIGINT, syscall.SIGTERM:
|
||||||
log.Println("shutting down ...")
|
log.Println("shutting down ...")
|
||||||
|
@ -44,7 +42,6 @@ func StopHandler(servers ...Stopper) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// StopReloaderHandler watches SIGINT, SIGTERM and SIGHUP on a list of servers
|
// StopReloaderHandler watches SIGINT, SIGTERM and SIGHUP on a list of servers
|
||||||
// implementing the StopReloader interface, and when one of those signals is
|
// implementing the StopReloader interface, and when one of those signals is
|
||||||
|
@ -54,9 +51,7 @@ func StopReloaderHandler(servers ...StopReloader) {
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
defer signal.Stop(signals)
|
defer signal.Stop(signals)
|
||||||
|
|
||||||
for {
|
for sig := range signals {
|
||||||
select {
|
|
||||||
case sig := <-signals:
|
|
||||||
switch sig {
|
switch sig {
|
||||||
case syscall.SIGHUP:
|
case syscall.SIGHUP:
|
||||||
log.Println("reloading ...")
|
log.Println("reloading ...")
|
||||||
|
@ -78,4 +73,3 @@ func StopReloaderHandler(servers ...StopReloader) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -553,7 +553,7 @@ func equalPools(a, b *x509.CertPool) bool {
|
||||||
for i := range subjects {
|
for i := range subjects {
|
||||||
sB[i] = string(subjects[i])
|
sB[i] = string(subjects[i])
|
||||||
}
|
}
|
||||||
sort.Sort(sort.StringSlice(sA))
|
sort.Strings(sA)
|
||||||
sort.Sort(sort.StringSlice(sB))
|
sort.Strings(sB)
|
||||||
return reflect.DeepEqual(sA, sB)
|
return reflect.DeepEqual(sA, sB)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ func (l *LoggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
rw := NewResponseLogger(w)
|
rw := NewResponseLogger(w)
|
||||||
l.next.ServeHTTP(rw, r)
|
l.next.ServeHTTP(rw, r)
|
||||||
d := time.Now().Sub(t)
|
d := time.Since(t)
|
||||||
l.writeEntry(rw, r, t, d)
|
l.writeEntry(rw, r, t, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue