diff --git a/api/api_test.go b/api/api_test.go index d101e04b..34844da3 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -682,13 +682,13 @@ func Test_caHandler_Provisioners(t *testing.T) { p := []*authority.Provisioner{ { Type: "JWK", - Issuer: "max", + Name: "max", EncryptedKey: "abc", Key: &key, }, { Type: "JWK", - Issuer: "mariano", + Name: "mariano", EncryptedKey: "def", Key: &key, }, diff --git a/authority/authority.go b/authority/authority.go index d324e915..5a59c492 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -115,7 +115,7 @@ func (a *Authority) init() error { } for _, p := range a.config.AuthorityConfig.Provisioners { - a.provisionerIDIndex.Store(p.Key.KeyID, p) + a.provisionerIDIndex.Store(p.ID(), p) if len(p.EncryptedKey) != 0 { a.encryptedKeyIndex.Store(p.Key.KeyID, p.EncryptedKey) } diff --git a/authority/authority_test.go b/authority/authority_test.go index 22aa0b5b..d6f04f06 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -17,14 +17,14 @@ func testAuthority(t *testing.T) *Authority { assert.FatalError(t, err) p := []*Provisioner{ { - Issuer: "Max", - Type: "JWK", - Key: maxjwk, + Name: "Max", + Type: "JWK", + Key: maxjwk, }, { - Issuer: "step-cli", - Type: "JWK", - Key: clijwk, + Name: "step-cli", + Type: "JWK", + Key: clijwk, }, } c := &Config{ @@ -113,7 +113,7 @@ func TestAuthorityNew(t *testing.T) { assert.True(t, auth.initOnce) assert.NotNil(t, auth.intermediateIdentity) for _, p := range tc.config.AuthorityConfig.Provisioners { - _p, ok := auth.provisionerIDIndex.Load(p.Key.KeyID) + _p, ok := auth.provisionerIDIndex.Load(p.ID()) assert.True(t, ok) assert.Equals(t, p, _p) if len(p.EncryptedKey) > 0 { diff --git a/authority/authorize.go b/authority/authorize.go index b131a95c..ce55f74a 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -44,14 +44,21 @@ func (a *Authority) Authorize(ott string) ([]interface{}, error) { http.StatusUnauthorized, errContext} } + // 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. + if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { + return nil, &apiError{err, http.StatusUnauthorized, errContext} + } kid := token.Headers[0].KeyID // JWT will only have 1 header. if len(kid) == 0 { return nil, &apiError{errors.New("authorize: token KeyID cannot be empty"), http.StatusUnauthorized, errContext} } - val, ok := a.provisionerIDIndex.Load(kid) + pid := claims.Issuer + ":" + kid + val, ok := a.provisionerIDIndex.Load(pid) if !ok { - return nil, &apiError{errors.Errorf("authorize: provisioner with KeyID %s not found", kid), + return nil, &apiError{errors.Errorf("authorize: provisioner with id %s not found", pid), http.StatusUnauthorized, errContext} } p, ok := val.(*Provisioner) @@ -67,7 +74,7 @@ func (a *Authority) Authorize(ott string) ([]interface{}, error) { // According to "rfc7519 JSON Web Token" acceptable skew should be no // more than a few minutes. if err = claims.ValidateWithLeeway(jwt.Expected{ - Issuer: p.Issuer, + Issuer: p.Name, }, time.Minute); err != nil { return nil, &apiError{errors.Wrapf(err, "authorize: invalid token"), http.StatusUnauthorized, errContext} diff --git a/authority/authorize_test.go b/authority/authorize_test.go index eb6d5fb6..d4416a25 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -121,7 +121,7 @@ func TestAuthorize(t *testing.T) { return &authorizeTest{ auth: a, ott: raw, - err: &apiError{errors.New("authorize: provisioner with KeyID foo not found"), + err: &apiError{errors.New("authorize: provisioner with id step-cli:foo not found"), http.StatusUnauthorized, context{"ott": raw}}, } }, @@ -132,7 +132,7 @@ func TestAuthorize(t *testing.T) { (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "foo")) assert.FatalError(t, err) - _a.provisionerIDIndex.Store("foo", "42") + _a.provisionerIDIndex.Store(validIssuer+":foo", "42") cl := jwt.Claims{ Subject: "test.smallstep.com", @@ -164,7 +164,7 @@ func TestAuthorize(t *testing.T) { return &authorizeTest{ auth: a, ott: raw, - err: &apiError{errors.New("authorize: invalid token"), + err: &apiError{errors.New("authorize: provisioner with id invalid-issuer:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc not found"), http.StatusUnauthorized, context{"ott": raw}}, } }, diff --git a/authority/config_test.go b/authority/config_test.go index 421275ef..ad4fc651 100644 --- a/authority/config_test.go +++ b/authority/config_test.go @@ -19,14 +19,14 @@ func TestConfigValidate(t *testing.T) { ac := &AuthConfig{ Provisioners: []*Provisioner{ { - Issuer: "Max", - Type: "JWK", - Key: maxjwk, + Name: "Max", + Type: "JWK", + Key: maxjwk, }, { - Issuer: "step-cli", - Type: "JWK", - Key: clijwk, + Name: "step-cli", + Type: "JWK", + Key: clijwk, }, }, } @@ -217,14 +217,14 @@ func TestAuthConfigValidate(t *testing.T) { assert.FatalError(t, err) p := []*Provisioner{ { - Issuer: "Max", - Type: "JWK", - Key: maxjwk, + Name: "Max", + Type: "JWK", + Key: maxjwk, }, { - Issuer: "step-cli", - Type: "JWK", - Key: clijwk, + Name: "step-cli", + Type: "JWK", + Key: clijwk, }, } @@ -250,8 +250,8 @@ func TestAuthConfigValidate(t *testing.T) { return AuthConfigValidateTest{ ac: &AuthConfig{ Provisioners: []*Provisioner{ - {Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}}, - {Issuer: "foo", Key: &jose.JSONWebKey{}}, + {Name: "foo", Type: "bar", Key: &jose.JSONWebKey{}}, + {Name: "foo", Key: &jose.JSONWebKey{}}, }, }, err: errors.New("provisioner type cannot be empty"), diff --git a/authority/provisioner.go b/authority/provisioner.go index 93732e2a..34a6e492 100644 --- a/authority/provisioner.go +++ b/authority/provisioner.go @@ -85,7 +85,7 @@ func (pc *ProvisionerClaims) Validate() error { // Provisioner - authorized entity that can sign tokens necessary for signature requests. type Provisioner struct { - Issuer string `json:"issuer,omitempty"` + Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Key *jose.JSONWebKey `json:"key,omitempty"` EncryptedKey string `json:"encryptedKey,omitempty"` @@ -95,7 +95,7 @@ type Provisioner struct { // Init initializes and validates a the fields of Provisioner type. func (p *Provisioner) Init(global *ProvisionerClaims) error { switch { - case p.Issuer == "": + case p.Name == "": return errors.New("provisioner issuer cannot be empty") case p.Type == "": @@ -117,7 +117,7 @@ func (p *Provisioner) getTLSApps(so SignOptions) ([]x509util.WithOption, []certC return []x509util.WithOption{ x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, c.DefaultTLSCertDuration()), - withProvisionerOID(p.Issuer, p.Key.KeyID), + withProvisionerOID(p.Name, p.Key.KeyID), }, []certClaim{ &certTemporalClaim{ min: c.MinTLSCertDuration(), @@ -125,3 +125,9 @@ func (p *Provisioner) getTLSApps(so SignOptions) ([]x509util.WithOption, []certC }, }, nil } + +// ID returns the provisioner identifier. The name and credential id should +// uniquely identify any provisioner. +func (p *Provisioner) ID() string { + return p.Name + ":" + p.Key.KeyID +} diff --git a/authority/provisioner_test.go b/authority/provisioner_test.go index 50d84127..0d33d679 100644 --- a/authority/provisioner_test.go +++ b/authority/provisioner_test.go @@ -22,19 +22,19 @@ func TestProvisionerInit(t *testing.T) { }, "fail-empty-type": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &Provisioner{Issuer: "foo"}, + p: &Provisioner{Name: "foo"}, err: errors.New("provisioner type cannot be empty"), } }, "fail-empty-key": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &Provisioner{Issuer: "foo", Type: "bar"}, + p: &Provisioner{Name: "foo", Type: "bar"}, err: errors.New("provisioner key cannot be empty"), } }, "ok": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &Provisioner{Issuer: "foo", Type: "bar", Key: &jose.JSONWebKey{}}, + p: &Provisioner{Name: "foo", Type: "bar", Key: &jose.JSONWebKey{}}, } }, } diff --git a/authority/provisioners_test.go b/authority/provisioners_test.go index fe7c0b43..07046d76 100644 --- a/authority/provisioners_test.go +++ b/authority/provisioners_test.go @@ -75,7 +75,7 @@ func TestGetEncryptedKey(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - val, ok := tc.a.provisionerIDIndex.Load(tc.kid) + val, ok := tc.a.provisionerIDIndex.Load("max:" + tc.kid) assert.Fatal(t, ok) p, ok := val.(*Provisioner) assert.Fatal(t, ok) @@ -155,7 +155,7 @@ func generateProvisioner(t *testing.T) *Provisioner { encrypted, err := jwe.CompactSerialize() assert.FatalError(t, err) return &Provisioner{ - Issuer: issuer, + Name: issuer, Type: "JWT", Key: &public, EncryptedKey: encrypted, diff --git a/authority/tls_test.go b/authority/tls_test.go index d5876716..4fedf379 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -204,7 +204,7 @@ func TestSign(t *testing.T) { _, err := asn1.Unmarshal(ext.Value, &val) assert.FatalError(t, err) assert.Equals(t, val.Type, provisionerTypeJWK) - assert.Equals(t, val.Name, []byte(p.Issuer)) + assert.Equals(t, val.Name, []byte(p.Name)) assert.Equals(t, val.CredentialID, []byte(p.Key.KeyID)) } assert.Equals(t, found, 1) diff --git a/ca/testdata/ca.json b/ca/testdata/ca.json index b8488b53..d61a8c49 100644 --- a/ca/testdata/ca.json +++ b/ca/testdata/ca.json @@ -20,7 +20,7 @@ "minCertDuration": "1m", "provisioners": [ { - "issuer": "max", + "name": "max", "type": "jwk", "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IkpsNkZLWUp4V1UwdGRIbG9UanA1aGcifQ.Qy0EP6u5-t0ggOweoc3Z1DCzR5BllsQi.KUkviZ_TJKY4c0Mi.h7QZqgh_Fl2MZpmVy4h375yC0DORjB1dQULbNqc6MuUCW2iweWVRysFImUXiXMUKRarJC5adwWy1GhyAqUj6Xj1iOZDGLjYnqMETGWcI0rKDBwcSU7y7Y-2VYBRDSM2b7aWtTBfz3_kvEaw_vc3b5CEPJ86UlZc-jhKFRr_IcGWU-vXX5-bppoH15IPreyzi55YdjCll338lYpDecB_Paym3XBXotyd2iGXXUwoA1npEFwuyRMMEhl9zLp7rVcMW6A_32EzB8cZANEnA0C4FXGHQalY6u_2UeqxcC8_FuXPay6VIYODyRqcABvvkft3nwOcrI0pYDGBdk2w2Euk.kOAFq3Tg6s4vBGS_plMpSw", "key": { @@ -33,7 +33,7 @@ "y": "ZhYcFQBqtErdC_pA7sOXrO7AboCEPIKP9Ik4CHJqANk" } }, { - "issuer": "max", + "name": "max", "type": "jwk", "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlZsWnl0dUxrWTR5enlqZXJybnN0aGcifQ.QP15wQYjZ12BLgl-XTq2Vb12G3OHAfic.X35QqAaXwnlmeCUU._2qIUp0TI8yDI7c2e9upIRdrnmB5OvtLfrYN-Su2NLBpaoYtr9O55Wo0Iryc0W2pYqnVDPvgPPes4P4nQAnzw5WhFYc1Xf1ZEetfdNhwi1x2FNwPbACBAgxm5AW40O5AAlbLcWushYASfeMBZocTGXuSGUzwFqoWD-5EDJ80TWQ7cAj3ttHrJ_3QV9hi4O9KJUCiXngN-Yz2zXrhBL4NOH2fmRbaf5c0rF8xUJIIW-TcyYJeX_Fbx1IzzKKPd9USUwkDhxD4tLa51I345xVqjuwG1PEn6nF8JKqLRVUKEKFin-ShXrfE61KceyAvm4YhWKrbJWIm3bH5Hxaphy4.TexIrIhsRxJStpE3EJ925Q", "key": { @@ -46,7 +46,7 @@ "y": "wnqZSMuXpmUxORq20t83LyY4BDYmqDGV9P7FGR6mw84" } }, { - "issuer": "step-cli", + "name": "step-cli", "type": "jwk", "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg", "key": { @@ -59,7 +59,7 @@ "y": "sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y" } }, { - "issuer": "mariano", + "name": "mariano", "type": "jwk", "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ", "key": { @@ -75,7 +75,7 @@ "minTLSCertDuration": "30s" } }, { - "issuer": "mariano", + "name": "mariano", "type": "jwk", "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6Ik5SLTk5ZkVMSm1CLW1FZGllUlFFc3cifQ.Fr314BEUGTda4ICJl2uxFdjpEUGGqJEV.gBbu_DZE1ONDu14r.X-7MKMyokZIF1HTCVqqL0tTWgaC1ZGZBLLltd11ZUhQTswo_8kvgiTv3cFShj7ATF0tAY8HStyJmzLO8mKPVOPDXSwjdNsPriZclI6JWGi9iOu8pEiN9pZM6-itxan1JMcDUNg2U-P1BmKppHRbDKsOTivymfRyeUk51dBIlS54p5xNK1HFLc1YtWC1Rc_ngYVqOgqlhIrCHArAEBe3jrfUaH2ym-8fkVdwVqtxmte3XXK9g8FchsygRNnOKtRcr0TyzTUV-7bPi8_t02Zi-EHLFaSawVXWV_Qk1GeLYJR22Rp74beo-b5-lCNVp10btO0xdGySUWmCJ4v4_QZw.c8unwWycwtfdJMM_0b0fuA", "key": {