Skip to content

Commit d6cad2a

Browse files
committedNov 1, 2018
Add provisioner option to disable renewal.
Fixes smallstep/ca-component#108
1 parent c74fcd5 commit d6cad2a

File tree

6 files changed

+111
-7
lines changed

6 files changed

+111
-7
lines changed
 

‎authority/authority_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func testAuthority(t *testing.T) *Authority {
1515
assert.FatalError(t, err)
1616
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
1717
assert.FatalError(t, err)
18+
disableRenewal := true
1819
p := []*Provisioner{
1920
{
2021
Name: "Max",
@@ -26,6 +27,14 @@ func testAuthority(t *testing.T) *Authority {
2627
Type: "JWK",
2728
Key: clijwk,
2829
},
30+
{
31+
Name: "dev",
32+
Type: "JWK",
33+
Key: maxjwk,
34+
Claims: &ProvisionerClaims{
35+
DisableRenewal: &disableRenewal,
36+
},
37+
},
2938
}
3039
c := &Config{
3140
Address: "127.0.0.1:443",

‎authority/authorize.go

+53
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package authority
22

33
import (
4+
"crypto/x509"
5+
"encoding/asn1"
46
"net/http"
57
"time"
68

@@ -117,3 +119,54 @@ func (a *Authority) Authorize(ott string) ([]interface{}, error) {
117119

118120
return signOps, nil
119121
}
122+
123+
// authorizeRenewal tries to locate the step provisioner extension, and checks
124+
// if for the configured provisioner, the renewal is enabled or not. If the
125+
// extra extension cannot be found, authorize the renewal by default.
126+
//
127+
// TODO(mariano): should we authorize by default?
128+
func (a *Authority) authorizeRenewal(crt *x509.Certificate) error {
129+
errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()}
130+
for _, e := range crt.Extensions {
131+
if e.Id.Equal(stepOIDProvisioner) {
132+
var provisioner stepProvisionerASN1
133+
if _, err := asn1.Unmarshal(e.Value, &provisioner); err != nil {
134+
return &apiError{
135+
err: errors.Wrap(err, "error decoding step provisioner extension"),
136+
code: http.StatusInternalServerError,
137+
context: errContext,
138+
}
139+
}
140+
141+
// Look for the provisioner, if it cannot be found, renewal will not
142+
// be authorized.
143+
pid := string(provisioner.Name) + ":" + string(provisioner.CredentialID)
144+
val, ok := a.provisionerIDIndex.Load(pid)
145+
if !ok {
146+
return &apiError{
147+
err: errors.Errorf("not found: provisioner %s", pid),
148+
code: http.StatusUnauthorized,
149+
context: errContext,
150+
}
151+
}
152+
p, ok := val.(*Provisioner)
153+
if !ok {
154+
return &apiError{
155+
err: errors.Errorf("invalid type: provisioner %s, type %T", pid, val),
156+
code: http.StatusInternalServerError,
157+
context: errContext,
158+
}
159+
}
160+
if p.Claims.IsDisableRenewal() {
161+
return &apiError{
162+
err: errors.Errorf("renew disabled: provisioner %s", pid),
163+
code: http.StatusUnauthorized,
164+
context: errContext,
165+
}
166+
}
167+
return nil
168+
}
169+
}
170+
171+
return nil
172+
}

‎authority/config.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ var (
2323
MaxVersion: 1.2,
2424
Renegotiation: false,
2525
}
26+
defaultDisableRenewal = false
2627
globalProvisionerClaims = ProvisionerClaims{
27-
MinTLSDur: &duration{5 * time.Minute},
28-
MaxTLSDur: &duration{24 * time.Hour},
29-
DefaultTLSDur: &duration{24 * time.Hour},
28+
MinTLSDur: &duration{5 * time.Minute},
29+
MaxTLSDur: &duration{24 * time.Hour},
30+
DefaultTLSDur: &duration{24 * time.Hour},
31+
DisableRenewal: &defaultDisableRenewal,
3032
}
3133
)
3234

‎authority/provisioner.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import (
1111

1212
// ProvisionerClaims so that individual provisioners can override global claims.
1313
type ProvisionerClaims struct {
14-
globalClaims *ProvisionerClaims
15-
MinTLSDur *duration `json:"minTLSCertDuration,omitempty"`
16-
MaxTLSDur *duration `json:"maxTLSCertDuration,omitempty"`
17-
DefaultTLSDur *duration `json:"defaultTLSCertDuration,omitempty"`
14+
globalClaims *ProvisionerClaims
15+
MinTLSDur *duration `json:"minTLSCertDuration,omitempty"`
16+
MaxTLSDur *duration `json:"maxTLSCertDuration,omitempty"`
17+
DefaultTLSDur *duration `json:"defaultTLSCertDuration,omitempty"`
18+
DisableRenewal *bool `json:"disableRenewal,omitempty"`
1819
}
1920

2021
// Init initializes and validates the individual provisioner claims.
@@ -57,6 +58,16 @@ func (pc *ProvisionerClaims) MaxTLSCertDuration() time.Duration {
5758
return pc.MaxTLSDur.Duration
5859
}
5960

61+
// IsDisableRenewal returns if the renewal flow is disabled for the
62+
// provisioner. If the property is not set withing the provisioner, then the
63+
// global value from the authority configuration will be used.
64+
func (pc *ProvisionerClaims) IsDisableRenewal() bool {
65+
if pc.DisableRenewal == nil {
66+
return pc.globalClaims.IsDisableRenewal()
67+
}
68+
return *pc.DisableRenewal
69+
}
70+
6071
// Validate validates and modifies the Claims with default values.
6172
func (pc *ProvisionerClaims) Validate() error {
6273
var (

‎authority/tls.go

+6
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, ext
169169
// Renew creates a new Certificate identical to the old certificate, except
170170
// with a validity window that begins 'now'.
171171
func (a *Authority) Renew(ocx *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) {
172+
// Check step provisioner extensions
173+
if err := a.authorizeRenewal(ocx); err != nil {
174+
return nil, nil, err
175+
}
176+
177+
// Issuer
172178
issIdentity := a.intermediateIdentity
173179

174180
// Convert a realx509.Certificate to the step x509 Certificate.

‎authority/tls_test.go

+23
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,19 @@ func TestRenew(t *testing.T) {
251251
crt, err := x509.ParseCertificate(crtBytes)
252252
assert.FatalError(t, err)
253253

254+
leafNoRenew, err := x509util.NewLeafProfile("norenew", a.intermediateIdentity.Crt,
255+
a.intermediateIdentity.Key,
256+
x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, 0),
257+
withDefaultASN1DN(a.config.AuthorityConfig.Template),
258+
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"),
259+
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].Key.KeyID),
260+
)
261+
assert.FatalError(t, err)
262+
crtBytesNoRenew, err := leafNoRenew.CreateCertificate()
263+
assert.FatalError(t, err)
264+
crtNoRenew, err := x509.ParseCertificate(crtBytesNoRenew)
265+
assert.FatalError(t, err)
266+
254267
type renewTest struct {
255268
auth *Authority
256269
crt *x509.Certificate
@@ -274,6 +287,16 @@ func TestRenew(t *testing.T) {
274287
http.StatusInternalServerError, context{}},
275288
}, nil
276289
},
290+
"fail-unauthorized": func() (*renewTest, error) {
291+
ctx := map[string]interface{}{
292+
"serialNumber": crtNoRenew.SerialNumber.String(),
293+
}
294+
return &renewTest{
295+
crt: crtNoRenew,
296+
err: &apiError{errors.New("renew disabled"),
297+
http.StatusUnauthorized, ctx},
298+
}, nil
299+
},
277300
"success": func() (*renewTest, error) {
278301
return &renewTest{
279302
crt: crt,

0 commit comments

Comments
 (0)