Skip to content

Commit

Permalink
feat: add hsm key set prefix to support multiple hydra instances on t…
Browse files Browse the repository at this point in the history
…he same hsm partition
  • Loading branch information
aarmam committed Apr 13, 2022
1 parent b548c5a commit 7170e31
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 13 deletions.
5 changes: 5 additions & 0 deletions driver/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion driver/registry_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions hsm/manager_hsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"fmt"
"net/http"
"sync"

Expand All @@ -32,6 +33,7 @@ type KeyManager struct {
jwk.Manager
sync.RWMutex
Context
KeyPrefix string
}

var ErrPreGeneratedKeys = &fosite.RFC6749Error{
Expand All @@ -40,16 +42,19 @@ 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,
}
}

func (m *KeyManager) GenerateAndPersistKeySet(_ context.Context, set, kid, alg, use string) (*jose.JSONWebKeySet, error) {
m.Lock()
defer m.Unlock()

set = m.prefixKeySet(set)

err := m.deleteExistingKeySet(set)
if err != nil {
return nil, err
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
102 changes: 93 additions & 9 deletions hsm/manager_hsm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"reflect"
"testing"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
},
Expand All @@ -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"))
},
Expand All @@ -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)
},
Expand All @@ -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"))
},
Expand All @@ -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)
},
Expand All @@ -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"))
},
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 2 additions & 1 deletion hsm/manager_nohsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions spec/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
}
},
Expand Down

0 comments on commit 7170e31

Please sign in to comment.