From d6cad2a7f33e97139f1476b2df2644f80d3e6e32 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 1 Nov 2018 15:43:24 -0700 Subject: [PATCH] Add provisioner option to disable renewal. Fixes smallstep/ca-component#108 --- authority/authority_test.go | 9 +++++++ authority/authorize.go | 53 +++++++++++++++++++++++++++++++++++++ authority/config.go | 8 +++--- authority/provisioner.go | 19 ++++++++++--- authority/tls.go | 6 +++++ authority/tls_test.go | 23 ++++++++++++++++ 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/authority/authority_test.go b/authority/authority_test.go index d6f04f06..71b51a52 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -15,6 +15,7 @@ func testAuthority(t *testing.T) *Authority { assert.FatalError(t, err) clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk") assert.FatalError(t, err) + disableRenewal := true p := []*Provisioner{ { Name: "Max", @@ -26,6 +27,14 @@ func testAuthority(t *testing.T) *Authority { Type: "JWK", Key: clijwk, }, + { + Name: "dev", + Type: "JWK", + Key: maxjwk, + Claims: &ProvisionerClaims{ + DisableRenewal: &disableRenewal, + }, + }, } c := &Config{ Address: "127.0.0.1:443", diff --git a/authority/authorize.go b/authority/authorize.go index ce55f74a..ca753cab 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -1,6 +1,8 @@ package authority import ( + "crypto/x509" + "encoding/asn1" "net/http" "time" @@ -117,3 +119,54 @@ func (a *Authority) Authorize(ott string) ([]interface{}, error) { return signOps, nil } + +// authorizeRenewal tries to locate the step provisioner extension, and checks +// if for the configured provisioner, the renewal is enabled or not. If the +// extra extension cannot be found, authorize the renewal by default. +// +// TODO(mariano): should we authorize by default? +func (a *Authority) authorizeRenewal(crt *x509.Certificate) error { + errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()} + for _, e := range crt.Extensions { + if e.Id.Equal(stepOIDProvisioner) { + var provisioner stepProvisionerASN1 + if _, err := asn1.Unmarshal(e.Value, &provisioner); err != nil { + return &apiError{ + err: errors.Wrap(err, "error decoding step provisioner extension"), + code: http.StatusInternalServerError, + context: errContext, + } + } + + // Look for the provisioner, if it cannot be found, renewal will not + // be authorized. + pid := string(provisioner.Name) + ":" + string(provisioner.CredentialID) + val, ok := a.provisionerIDIndex.Load(pid) + if !ok { + return &apiError{ + err: errors.Errorf("not found: provisioner %s", pid), + code: http.StatusUnauthorized, + context: errContext, + } + } + p, ok := val.(*Provisioner) + if !ok { + return &apiError{ + err: errors.Errorf("invalid type: provisioner %s, type %T", pid, val), + code: http.StatusInternalServerError, + context: errContext, + } + } + if p.Claims.IsDisableRenewal() { + return &apiError{ + err: errors.Errorf("renew disabled: provisioner %s", pid), + code: http.StatusUnauthorized, + context: errContext, + } + } + return nil + } + } + + return nil +} diff --git a/authority/config.go b/authority/config.go index 37a0dec6..5f41b48f 100644 --- a/authority/config.go +++ b/authority/config.go @@ -23,10 +23,12 @@ var ( MaxVersion: 1.2, Renegotiation: false, } + defaultDisableRenewal = false globalProvisionerClaims = ProvisionerClaims{ - MinTLSDur: &duration{5 * time.Minute}, - MaxTLSDur: &duration{24 * time.Hour}, - DefaultTLSDur: &duration{24 * time.Hour}, + MinTLSDur: &duration{5 * time.Minute}, + MaxTLSDur: &duration{24 * time.Hour}, + DefaultTLSDur: &duration{24 * time.Hour}, + DisableRenewal: &defaultDisableRenewal, } ) diff --git a/authority/provisioner.go b/authority/provisioner.go index bfaf8d7c..444b1722 100644 --- a/authority/provisioner.go +++ b/authority/provisioner.go @@ -11,10 +11,11 @@ import ( // ProvisionerClaims so that individual provisioners can override global claims. type ProvisionerClaims struct { - globalClaims *ProvisionerClaims - MinTLSDur *duration `json:"minTLSCertDuration,omitempty"` - MaxTLSDur *duration `json:"maxTLSCertDuration,omitempty"` - DefaultTLSDur *duration `json:"defaultTLSCertDuration,omitempty"` + globalClaims *ProvisionerClaims + MinTLSDur *duration `json:"minTLSCertDuration,omitempty"` + MaxTLSDur *duration `json:"maxTLSCertDuration,omitempty"` + DefaultTLSDur *duration `json:"defaultTLSCertDuration,omitempty"` + DisableRenewal *bool `json:"disableRenewal,omitempty"` } // Init initializes and validates the individual provisioner claims. @@ -57,6 +58,16 @@ func (pc *ProvisionerClaims) MaxTLSCertDuration() time.Duration { return pc.MaxTLSDur.Duration } +// IsDisableRenewal returns if the renewal flow is disabled for the +// provisioner. If the property is not set withing the provisioner, then the +// global value from the authority configuration will be used. +func (pc *ProvisionerClaims) IsDisableRenewal() bool { + if pc.DisableRenewal == nil { + return pc.globalClaims.IsDisableRenewal() + } + return *pc.DisableRenewal +} + // Validate validates and modifies the Claims with default values. func (pc *ProvisionerClaims) Validate() error { var ( diff --git a/authority/tls.go b/authority/tls.go index e7267f6b..750ced60 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -169,6 +169,12 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, ext // Renew creates a new Certificate identical to the old certificate, except // with a validity window that begins 'now'. func (a *Authority) Renew(ocx *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) { + // Check step provisioner extensions + if err := a.authorizeRenewal(ocx); err != nil { + return nil, nil, err + } + + // Issuer issIdentity := a.intermediateIdentity // Convert a realx509.Certificate to the step x509 Certificate. diff --git a/authority/tls_test.go b/authority/tls_test.go index 4fedf379..39ba4b28 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -251,6 +251,19 @@ func TestRenew(t *testing.T) { crt, err := x509.ParseCertificate(crtBytes) assert.FatalError(t, err) + leafNoRenew, err := x509util.NewLeafProfile("norenew", a.intermediateIdentity.Crt, + a.intermediateIdentity.Key, + x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, 0), + withDefaultASN1DN(a.config.AuthorityConfig.Template), + x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"), + withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].Key.KeyID), + ) + assert.FatalError(t, err) + crtBytesNoRenew, err := leafNoRenew.CreateCertificate() + assert.FatalError(t, err) + crtNoRenew, err := x509.ParseCertificate(crtBytesNoRenew) + assert.FatalError(t, err) + type renewTest struct { auth *Authority crt *x509.Certificate @@ -274,6 +287,16 @@ func TestRenew(t *testing.T) { http.StatusInternalServerError, context{}}, }, nil }, + "fail-unauthorized": func() (*renewTest, error) { + ctx := map[string]interface{}{ + "serialNumber": crtNoRenew.SerialNumber.String(), + } + return &renewTest{ + crt: crtNoRenew, + err: &apiError{errors.New("renew disabled"), + http.StatusUnauthorized, ctx}, + }, nil + }, "success": func() (*renewTest, error) { return &renewTest{ crt: crt,