diff --git a/driver/config/provider.go b/driver/config/provider.go index fc39ee4a96e..96470cd2cbe 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -29,6 +29,7 @@ const ( HsmLibraryPath = "hsm.library" HsmPin = "hsm.pin" HsmSlotNumber = "hsm.slot" + HsmKeySetPrefix = "hsm.key_set_prefix" HsmTokenLabel = "hsm.token_label" // #nosec G101 KeyWellKnownKeys = "webfinger.jwks.broadcast_keys" KeyOAuth2ClientRegistrationURL = "webfinger.oidc_discovery.client_registration_url" @@ -469,6 +470,10 @@ func (p *Provider) HsmTokenLabel() string { return p.p.String(HsmTokenLabel) } +func (p *Provider) HsmKeyPrefix() string { + return p.p.String(HsmKeySetPrefix) +} + func (p *Provider) GrantTypeJWTBearerIDOptional() bool { return p.p.Bool(KeyOAuth2GrantJWTIDOptional) } diff --git a/driver/registry_sql.go b/driver/registry_sql.go index 96583ba5008..8cf9267580d 100644 --- a/driver/registry_sql.go +++ b/driver/registry_sql.go @@ -86,7 +86,7 @@ func (m *RegistrySQL) Init(ctx context.Context) error { } if m.C.HsmEnabled() { - hardwareKeyManager := hsm.NewKeyManager(m.HsmContext()) + hardwareKeyManager := hsm.NewKeyManager(m.HsmContext(), m.C.HsmKeyPrefix()) m.defaultKeyManager = jwk.NewManagerStrategy(hardwareKeyManager, m.persister) } else { m.defaultKeyManager = m.persister diff --git a/hsm/manager_hsm.go b/hsm/manager_hsm.go index 928a61ce960..827016d2dac 100644 --- a/hsm/manager_hsm.go +++ b/hsm/manager_hsm.go @@ -9,6 +9,7 @@ import ( "crypto/elliptic" "crypto/rsa" "crypto/x509" + "fmt" "net/http" "sync" @@ -32,6 +33,7 @@ type KeyManager struct { jwk.Manager sync.RWMutex Context + KeyPrefix string } var ErrPreGeneratedKeys = &fosite.RFC6749Error{ @@ -40,9 +42,10 @@ var ErrPreGeneratedKeys = &fosite.RFC6749Error{ DescriptionField: "Cannot add/update pre generated keys on Hardware Security Module", } -func NewKeyManager(hsm Context) *KeyManager { +func NewKeyManager(hsm Context, keyPrefix string) *KeyManager { return &KeyManager{ - Context: hsm, + Context: hsm, + KeyPrefix: keyPrefix, } } @@ -50,6 +53,8 @@ func (m *KeyManager) GenerateAndPersistKeySet(_ context.Context, set, kid, alg, m.Lock() defer m.Unlock() + set = m.prefixKeySet(set) + err := m.deleteExistingKeySet(set) if err != nil { return nil, err @@ -99,6 +104,8 @@ func (m *KeyManager) GetKey(_ context.Context, set, kid string) (*jose.JSONWebKe m.RLock() defer m.RUnlock() + set = m.prefixKeySet(set) + keyPair, err := m.FindKeyPair([]byte(kid), []byte(set)) if err != nil { return nil, err @@ -120,6 +127,8 @@ func (m *KeyManager) GetKeySet(_ context.Context, set string) (*jose.JSONWebKeyS m.RLock() defer m.RUnlock() + set = m.prefixKeySet(set) + keyPairs, err := m.FindKeyPairs(nil, []byte(set)) if err != nil { return nil, err @@ -147,6 +156,8 @@ func (m *KeyManager) DeleteKey(_ context.Context, set, kid string) error { m.Lock() defer m.Unlock() + set = m.prefixKeySet(set) + keyPair, err := m.FindKeyPair([]byte(kid), []byte(set)) if err != nil { return err @@ -167,6 +178,8 @@ func (m *KeyManager) DeleteKeySet(_ context.Context, set string) error { m.Lock() defer m.Unlock() + set = m.prefixKeySet(set) + keyPairs, err := m.FindKeyPairs(nil, []byte(set)) if err != nil { return err @@ -309,3 +322,7 @@ func createKeys(key crypto11.Signer, kid, alg, use string) []jose.JSONWebKey { CertificateThumbprintSHA256: []uint8{}, }} } + +func (m *KeyManager) prefixKeySet(set string) string { + return fmt.Sprintf("%s%s", m.KeyPrefix, set) +} diff --git a/hsm/manager_hsm_test.go b/hsm/manager_hsm_test.go index ab7d7ec859c..ed2668905ca 100644 --- a/hsm/manager_hsm_test.go +++ b/hsm/manager_hsm_test.go @@ -10,6 +10,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "fmt" "reflect" "testing" @@ -53,6 +54,89 @@ func TestDefaultKeyManager_HsmEnabled(t *testing.T) { assert.IsType(t, &sql.Persister{}, reg.SoftwareKeyManager()) } +func TestKeyManager_HsmKeyPrefix(t *testing.T) { + ctrl := gomock.NewController(t) + hsmContext := NewMockContext(ctrl) + defer ctrl.Finish() + + rsaKey, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err) + + ecdsaKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + require.NoError(t, err) + + rsaKeyPair := NewMockSignerDecrypter(ctrl) + rsaKeyPair.EXPECT().Public().Return(&rsaKey.PublicKey).AnyTimes() + + ecdsaKeyPair := NewMockSignerDecrypter(ctrl) + ecdsaKeyPair.EXPECT().Public().Return(&ecdsaKey.PublicKey).AnyTimes() + + var kid = uuid.New() + + keyPrefix := "application_specific_prefix." + expectedPrefixedOpenIDConnectKeyName := fmt.Sprintf("%s%s", keyPrefix, x.OpenIDConnectKeyName) + + m := &hsm.KeyManager{ + Context: hsmContext, + KeyPrefix: keyPrefix, + } + + t.Run("case=GenerateAndPersistKeySet", func(t *testing.T) { + privateAttrSet, publicAttrSet := expectedKeyAttributes(t, expectedPrefixedOpenIDConnectKeyName, kid) + hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(expectedPrefixedOpenIDConnectKeyName))).Return(nil, nil) + hsmContext.EXPECT().GenerateRSAKeyPairWithAttributes(gomock.Eq(publicAttrSet), gomock.Eq(privateAttrSet), gomock.Eq(4096)).Return(rsaKeyPair, nil) + + got, err := m.GenerateAndPersistKeySet(context.TODO(), x.OpenIDConnectKeyName, kid, "RS256", "sig") + + assert.NoError(t, err) + expectedKeySet := expectedKeySet(rsaKeyPair, kid, "RS256", "sig") + if !reflect.DeepEqual(got, expectedKeySet) { + t.Errorf("GenerateAndPersistKeySet() got = %v, want %v", got, expectedKeySet) + } + }) + t.Run("case=GetKey", func(t *testing.T) { + hsmContext.EXPECT().FindKeyPair(gomock.Eq([]byte(kid)), gomock.Eq([]byte(expectedPrefixedOpenIDConnectKeyName))).Return(rsaKeyPair, nil) + hsmContext.EXPECT().GetAttribute(gomock.Eq(rsaKeyPair), gomock.Eq(crypto11.CkaDecrypt)).Return(nil, nil) + + got, err := m.GetKey(context.TODO(), x.OpenIDConnectKeyName, kid) + + assert.NoError(t, err) + expectedKeySet := expectedKeySet(rsaKeyPair, kid, "RS256", "sig") + if !reflect.DeepEqual(got, expectedKeySet) { + t.Errorf("GetKey() got = %v, want %v", got, expectedKeySet) + } + }) + t.Run("case=GetKeySet", func(t *testing.T) { + hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(expectedPrefixedOpenIDConnectKeyName))).Return([]crypto11.Signer{rsaKeyPair}, nil) + hsmContext.EXPECT().GetAttribute(gomock.Eq(rsaKeyPair), gomock.Eq(crypto11.CkaId)).Return(pkcs11.NewAttribute(pkcs11.CKA_ID, []byte(kid)), nil) + hsmContext.EXPECT().GetAttribute(gomock.Eq(rsaKeyPair), gomock.Eq(crypto11.CkaDecrypt)).Return(nil, nil) + + got, err := m.GetKeySet(context.TODO(), x.OpenIDConnectKeyName) + + assert.NoError(t, err) + expectedKeySet := expectedKeySet(rsaKeyPair, kid, "RS256", "sig") + if !reflect.DeepEqual(got, expectedKeySet) { + t.Errorf("GetKey() got = %v, want %v", got, expectedKeySet) + } + }) + t.Run("case=DeleteKey", func(t *testing.T) { + hsmContext.EXPECT().FindKeyPair(gomock.Eq([]byte(kid)), gomock.Eq([]byte(expectedPrefixedOpenIDConnectKeyName))).Return(rsaKeyPair, nil) + rsaKeyPair.EXPECT().Delete().Return(nil) + + err := m.DeleteKey(context.TODO(), x.OpenIDConnectKeyName, kid) + + assert.NoError(t, err) + }) + t.Run("case=DeleteKeySet", func(t *testing.T) { + hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(expectedPrefixedOpenIDConnectKeyName))).Return([]crypto11.Signer{rsaKeyPair}, nil) + rsaKeyPair.EXPECT().Delete().Return(nil) + + err := m.DeleteKeySet(context.TODO(), x.OpenIDConnectKeyName) + + assert.NoError(t, err) + }) +} + func TestKeyManager_GenerateAndPersistKeySet(t *testing.T) { ctrl := gomock.NewController(t) hsmContext := NewMockContext(ctrl) @@ -97,7 +181,7 @@ func TestKeyManager_GenerateAndPersistKeySet(t *testing.T) { use: "sig", }, setup: func(t *testing.T) { - privateAttrSet, publicAttrSet := expectedKeyAttributes(t, kid) + privateAttrSet, publicAttrSet := expectedKeyAttributes(t, x.OpenIDConnectKeyName, kid) hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(x.OpenIDConnectKeyName))).Return(nil, nil) hsmContext.EXPECT().GenerateRSAKeyPairWithAttributes(gomock.Eq(publicAttrSet), gomock.Eq(privateAttrSet), gomock.Eq(4096)).Return(rsaKeyPair, nil) }, @@ -113,7 +197,7 @@ func TestKeyManager_GenerateAndPersistKeySet(t *testing.T) { use: "sig", }, setup: func(t *testing.T) { - privateAttrSet, publicAttrSet := expectedKeyAttributes(t, kid) + privateAttrSet, publicAttrSet := expectedKeyAttributes(t, x.OpenIDConnectKeyName, kid) hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(x.OpenIDConnectKeyName))).Return(nil, nil) hsmContext.EXPECT().GenerateRSAKeyPairWithAttributes(gomock.Eq(publicAttrSet), gomock.Eq(privateAttrSet), gomock.Eq(4096)).Return(nil, errors.New("GenerateRSAKeyPairWithAttributesError")) }, @@ -129,7 +213,7 @@ func TestKeyManager_GenerateAndPersistKeySet(t *testing.T) { use: "sig", }, setup: func(t *testing.T) { - privateAttrSet, publicAttrSet := expectedKeyAttributes(t, kid) + privateAttrSet, publicAttrSet := expectedKeyAttributes(t, x.OpenIDConnectKeyName, kid) hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(x.OpenIDConnectKeyName))).Return(nil, nil) hsmContext.EXPECT().GenerateECDSAKeyPairWithAttributes(gomock.Eq(publicAttrSet), gomock.Eq(privateAttrSet), gomock.Eq(elliptic.P256())).Return(ecdsaKeyPair, nil) }, @@ -145,7 +229,7 @@ func TestKeyManager_GenerateAndPersistKeySet(t *testing.T) { use: "sig", }, setup: func(t *testing.T) { - privateAttrSet, publicAttrSet := expectedKeyAttributes(t, kid) + privateAttrSet, publicAttrSet := expectedKeyAttributes(t, x.OpenIDConnectKeyName, kid) hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(x.OpenIDConnectKeyName))).Return(nil, nil) hsmContext.EXPECT().GenerateECDSAKeyPairWithAttributes(gomock.Eq(publicAttrSet), gomock.Eq(privateAttrSet), gomock.Eq(elliptic.P256())).Return(nil, errors.New("GenerateECDSAKeyPairWithAttributesError")) }, @@ -161,7 +245,7 @@ func TestKeyManager_GenerateAndPersistKeySet(t *testing.T) { use: "sig", }, setup: func(t *testing.T) { - privateAttrSet, publicAttrSet := expectedKeyAttributes(t, kid) + privateAttrSet, publicAttrSet := expectedKeyAttributes(t, x.OpenIDConnectKeyName, kid) hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(x.OpenIDConnectKeyName))).Return(nil, nil) hsmContext.EXPECT().GenerateECDSAKeyPairWithAttributes(gomock.Eq(publicAttrSet), gomock.Eq(privateAttrSet), gomock.Eq(elliptic.P521())).Return(ecdsaKeyPair, nil) }, @@ -177,7 +261,7 @@ func TestKeyManager_GenerateAndPersistKeySet(t *testing.T) { use: "sig", }, setup: func(t *testing.T) { - privateAttrSet, publicAttrSet := expectedKeyAttributes(t, kid) + privateAttrSet, publicAttrSet := expectedKeyAttributes(t, x.OpenIDConnectKeyName, kid) hsmContext.EXPECT().FindKeyPairs(gomock.Nil(), gomock.Eq([]byte(x.OpenIDConnectKeyName))).Return(nil, nil) hsmContext.EXPECT().GenerateECDSAKeyPairWithAttributes(gomock.Eq(publicAttrSet), gomock.Eq(privateAttrSet), gomock.Eq(elliptic.P521())).Return(nil, errors.New("GenerateECDSAKeyPairWithAttributesError")) }, @@ -766,10 +850,10 @@ func TestKeyManager_UpdateKeySet(t *testing.T) { assert.ErrorIs(t, err, hsm.ErrPreGeneratedKeys) } -func expectedKeyAttributes(t *testing.T, kid string) (crypto11.AttributeSet, crypto11.AttributeSet) { - privateAttrSet, err := crypto11.NewAttributeSetWithIDAndLabel([]byte(kid), []byte(x.OpenIDConnectKeyName)) +func expectedKeyAttributes(t *testing.T, set, kid string) (crypto11.AttributeSet, crypto11.AttributeSet) { + privateAttrSet, err := crypto11.NewAttributeSetWithIDAndLabel([]byte(kid), []byte(set)) require.NoError(t, err) - publicAttrSet, err := crypto11.NewAttributeSetWithIDAndLabel([]byte(kid), []byte(x.OpenIDConnectKeyName)) + publicAttrSet, err := crypto11.NewAttributeSetWithIDAndLabel([]byte(kid), []byte(set)) require.NoError(t, err) publicAttrSet.AddIfNotPresent([]*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_VERIFY, true), diff --git a/hsm/manager_nohsm.go b/hsm/manager_nohsm.go index 2e20d9a7891..8ce6fdc258d 100644 --- a/hsm/manager_nohsm.go +++ b/hsm/manager_nohsm.go @@ -24,6 +24,7 @@ type KeyManager struct { jwk.Manager sync.RWMutex Context + KeyPrefix string } var ErrOpSysNotSupported = errors.New("Hardware Security Module is not supported on this platform.") @@ -33,7 +34,7 @@ func NewContext(c *config.Provider, l *logrusx.Logger) Context { return nil } -func NewKeyManager(hsm Context) *KeyManager { +func NewKeyManager(hsm Context, keyPrefix string) *KeyManager { return nil } diff --git a/internal/config/config.yaml b/internal/config/config.yaml index 868d7918ae4..e4d759c5593 100644 --- a/internal/config/config.yaml +++ b/internal/config/config.yaml @@ -275,6 +275,10 @@ hsm: pin: token-pin-code slot: 0 token_label: hydra + # Key set prefix can be used in case of multiple hydra instances use the same HSM partition. + # For example key set hydra.openid.id-token would be generated/requested/deleted on HSM with + # CKA_LABEL=app1.hydra.openid.id-token. + key_set_prefix: app1. # webfinger configures ./well-known/ settings webfinger: diff --git a/spec/config.json b/spec/config.json index 0e8b7baa1b5..732a29b47f9 100644 --- a/spec/config.json +++ b/spec/config.json @@ -479,6 +479,11 @@ "token_label": { "type": "string", "description": "Label of the token to use (if slot is not specified). If both slot and label are set, token label takes preference over slot. In this case first slot, that contains this label is used." + }, + "key_set_prefix": { + "type": "string", + "description": "Key set prefix can be used in case of multiple hydra instances use the same HSM partition. For example key set hydra.openid.id-token would be generated/requested/deleted on HSM with CKA_LABEL=app1.hydra.openid.id-token.", + "default": "" } } },