Skip to content

Crypto: alter Storage interface to create keys inside key store #3120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 4 additions & 38 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ package crypto
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"github.com/lestrrat-go/jwx/v2/jwk"
Expand Down Expand Up @@ -153,24 +150,13 @@ func (client *Crypto) Configure(config core.ServerConfig) error {
// Stores the private key, returns the public basicKey.
// It returns an error when a key with the resulting ID already exists.
func (client *Crypto) New(ctx context.Context, namingFunc KIDNamingFunc) (Key, error) {
keyPair, kid, err := generateKeyPairAndKID(namingFunc)
publicKey, kid, err := client.storage.NewPrivateKey(ctx, namingFunc)
if err != nil {
return nil, err
}

audit.Log(ctx, log.Logger(), audit.CryptoNewKeyEvent).Infof("Generating new key pair: %s", kid)
exists, err := client.storage.PrivateKeyExists(ctx, kid)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("key with the given ID already exists")
}
if err = client.storage.SavePrivateKey(ctx, kid, keyPair); err != nil {
return nil, fmt.Errorf("could not create new keypair: could not save private key: %w", err)
}
audit.Log(ctx, log.Logger(), audit.CryptoNewKeyEvent).Infof("Generated new key pair: %s", kid)
return basicKey{
publicKey: keyPair.Public(),
publicKey: publicKey,
kid: kid,
}, nil
}
Expand All @@ -181,30 +167,10 @@ func (client *Crypto) Delete(ctx context.Context, kid string) error {
return client.storage.DeletePrivateKey(ctx, kid)
}

func generateKeyPairAndKID(namingFunc KIDNamingFunc) (*ecdsa.PrivateKey, string, error) {
keyPair, err := generateECKeyPair()
if err != nil {
return nil, "", err
}

kid, err := namingFunc(keyPair.Public())
if err != nil {
return nil, "", err
}
log.Logger().
WithField(core.LogFieldKeyID, kid).
Debug("Generated new key pair")
return keyPair, kid, nil
}

func generateECKeyPair() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}

// GenerateJWK a new in-memory key pair and returns it as JWK.
// It sets the alg field of the JWK.
func GenerateJWK() (jwk.Key, error) {
keyPair, err := generateECKeyPair()
keyPair, err := spi.GenerateKeyPair()
if err != nil {
return nil, nil
}
Expand Down
36 changes: 1 addition & 35 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ package crypto

import (
"context"
"crypto"
"errors"
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/crypto/storage/fs"
"github.com/nuts-foundation/nuts-node/crypto/storage/spi"
Expand Down Expand Up @@ -81,7 +79,7 @@ func TestCrypto_New(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, key.Public())
assert.Equal(t, kid, key.KID())
auditLogs.AssertContains(t, ModuleName, "CreateNewKey", audit.TestActor, "Generating new key pair: kid")
auditLogs.AssertContains(t, ModuleName, "CreateNewKey", audit.TestActor, "Generated new key pair: kid")
})

t.Run("error - invalid KID", func(t *testing.T) {
Expand All @@ -92,38 +90,6 @@ func TestCrypto_New(t *testing.T) {
assert.ErrorContains(t, err, "invalid key ID")
assert.Nil(t, key)
})

t.Run("error - NamingFunction returns err", func(t *testing.T) {
errorNamingFunc := func(key crypto.PublicKey) (string, error) {
return "", errors.New("b00m!")
}
_, err := client.New(ctx, errorNamingFunc)
assert.Error(t, err)
})

t.Run("error - save public key returns an error", func(t *testing.T) {
ctrl := gomock.NewController(t)
storageMock := spi.NewMockStorage(ctrl)
storageMock.EXPECT().PrivateKeyExists(ctx, "123").Return(false, nil)
storageMock.EXPECT().SavePrivateKey(ctx, gomock.Any(), gomock.Any()).Return(errors.New("foo"))

client := &Crypto{storage: storageMock}
key, err := client.New(ctx, StringNamingFunc("123"))
assert.Nil(t, key)
assert.Error(t, err)
assert.Equal(t, "could not create new keypair: could not save private key: foo", err.Error())
})

t.Run("error - ID already in use", func(t *testing.T) {
ctrl := gomock.NewController(t)
storageMock := spi.NewMockStorage(ctrl)
storageMock.EXPECT().PrivateKeyExists(ctx, "123").Return(true, nil)

client := &Crypto{storage: storageMock}
key, err := client.New(ctx, StringNamingFunc("123"))
assert.Nil(t, key)
assert.EqualError(t, err, "key with the given ID already exists", err)
})
}

func TestCrypto_Delete(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions crypto/ecies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
package crypto

import (
"github.com/nuts-foundation/nuts-node/crypto/storage/spi"
"testing"

"github.com/stretchr/testify/assert"
)

func TestEciesEncrypt(t *testing.T) {
key, err := generateECKeyPair()
key, err := spi.GenerateKeyPair()
assert.NoError(t, err)

cipherText1, err := EciesEncrypt(&key.PublicKey, []byte("hello world"))
Expand All @@ -38,7 +39,7 @@ func TestEciesEncrypt(t *testing.T) {
}

func TestEciesDecrypt(t *testing.T) {
key, err := generateECKeyPair()
key, err := spi.GenerateKeyPair()
assert.NoError(t, err)

cipherText, err := EciesEncrypt(&key.PublicKey, []byte("hello world"))
Expand Down
6 changes: 5 additions & 1 deletion crypto/ephemeral.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@

package crypto

import (
"github.com/nuts-foundation/nuts-node/crypto/storage/spi"
)

// NewEphemeralKey returns a Key for single use.
func NewEphemeralKey(namingFunc KIDNamingFunc) (Key, error) {
keyPair, kid, err := generateKeyPairAndKID(namingFunc)
keyPair, kid, err := spi.GenerateKeyPairAndKID(namingFunc)
if err != nil {
return nil, err
}
Expand Down
4 changes: 4 additions & 0 deletions crypto/storage/external/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type APIClient struct {
httpClient *ClientWithResponses
}

func (c APIClient) NewPrivateKey(ctx context.Context, namingFunc func(crypto.PublicKey) (string, error)) (crypto.PublicKey, string, error) {
return spi.GenerateAndStore(ctx, c, namingFunc)
}

func (c APIClient) Name() string {
return "Crypto"
}
Expand Down
4 changes: 4 additions & 0 deletions crypto/storage/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ func NewFileSystemBackend(fspath string) (spi.Storage, error) {
return fsc, nil
}

func (fsc fileSystemBackend) NewPrivateKey(ctx context.Context, namingFunc func(crypto.PublicKey) (string, error)) (crypto.PublicKey, string, error) {
return spi.GenerateAndStore(ctx, fsc, namingFunc)
}

func (fsc fileSystemBackend) PrivateKeyExists(_ context.Context, kid string) (bool, error) {
_, err := os.Stat(fsc.getEntryPath(kid, privateKeyEntry))
if errors.Is(err, os.ErrNotExist) {
Expand Down
50 changes: 49 additions & 1 deletion crypto/storage/spi/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ package spi
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
Expand All @@ -42,11 +45,15 @@ var KidPattern = regexp.MustCompile(`^(?:(?:[\da-zA-Z_\- :#.])|(?:%[0-9a-fA-F]{2
// Storage interface containing functions for storing and retrieving keys.
type Storage interface {
core.HealthCheckable
// NewPrivateKey creates a new private key and returns its handler as an implementation of crypto.Signer.
// It should be preferred over generating a key in the application and saving it to the storage,
// as it allows for unexportable (safer) keys. If the resulting kid already exists, it returns an error
NewPrivateKey(ctx context.Context, namingFunc func(crypto.PublicKey) (string, error)) (crypto.PublicKey, string, error)
// GetPrivateKey from the storage backend and return its handler as an implementation of crypto.Signer.
GetPrivateKey(ctx context.Context, kid string) (crypto.Signer, error)
// PrivateKeyExists checks if the private key indicated with the kid is stored in the storage backend.
PrivateKeyExists(ctx context.Context, kid string) (bool, error)
// SavePrivateKey stores the key under the kid in the storage backend.
// SavePrivateKey imports the key under the kid in the storage backend.
SavePrivateKey(ctx context.Context, kid string, key crypto.PrivateKey) error
// ListPrivateKeys returns the KIDs of the private keys that are present. Returns a []string(nil) if there was a problem.
ListPrivateKeys(ctx context.Context) []string
Expand Down Expand Up @@ -96,3 +103,44 @@ func (pke *PublicKeyEntry) UnmarshalJSON(bytes []byte) error {
func (pke PublicKeyEntry) JWK() jwk.Key {
return pke.parsedJWK
}

// GenerateAndStore generates a new key pair and stores it in the provided storage.
func GenerateAndStore(ctx context.Context, store Storage, namingFunc func(crypto.PublicKey) (string, error)) (crypto.PublicKey, string, error) {
keyPair, kid, err := GenerateKeyPairAndKID(namingFunc)
if err != nil {
return nil, "", err
}
exists, err := store.PrivateKeyExists(ctx, kid)
if err != nil {
return nil, "", err
}
if exists {
return nil, "", errors.New("key with the given ID already exists")
}
if err = store.SavePrivateKey(ctx, kid, keyPair); err != nil {
return nil, "", fmt.Errorf("could not create new keypair: could not save private key: %w", err)
}
return keyPair.Public(), kid, nil
}

// GenerateKeyPair generates a new key pair using the default key type.
// It's intended to be used by crypto backends that don't create unexportable keys.
func GenerateKeyPair() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}

// GenerateKeyPairAndKID generates a new key pair and a KID using the provided naming function.
// It's intended to be used by crypto backends that don't create unexportable keys.
func GenerateKeyPairAndKID(namingFunc func(crypto.PublicKey) (string, error)) (*ecdsa.PrivateKey, string, error) {
keyPair, err := GenerateKeyPair()
if err != nil {
return nil, "", err
}

kid, err := namingFunc(keyPair.Public())
if err != nil {
return nil, "", err
}

return keyPair, kid, nil
}
71 changes: 71 additions & 0 deletions crypto/storage/spi/interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
package spi

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -49,3 +53,70 @@ func TestPublicKeyEntry_FromJWK(t *testing.T) {
assert.NotEmpty(t, entry.Key)
assert.Same(t, pk, entry.parsedJWK)
}

func TestGenerateKeyPairAndKID(t *testing.T) {
t.Run("success", func(t *testing.T) {
keyPair, kid, err := GenerateKeyPairAndKID(func(_ crypto.PublicKey) (string, error) {
return "keyname", nil
})
require.NoError(t, err)
assert.NotNil(t, keyPair)
assert.Equal(t, "keyname", kid)
})
}

func TestGenerateAndStore(t *testing.T) {
ctx := context.Background()
t.Run("ok", func(t *testing.T) {
ctrl := gomock.NewController(t)
store := NewMockStorage(ctrl)
store.EXPECT().PrivateKeyExists(ctx, "123").Return(false, nil)
store.EXPECT().SavePrivateKey(ctx, gomock.Any(), gomock.Any()).Return(nil)
kid := "123"

key, kid, err := GenerateAndStore(ctx, store, func(_ crypto.PublicKey) (string, error) {
return kid, nil
})

assert.NoError(t, err)
assert.NotNil(t, key)
assert.Equal(t, "123", kid)
})
t.Run("error - NamingFunction returns err", func(t *testing.T) {
ctrl := gomock.NewController(t)
store := NewMockStorage(ctrl)

_, _, err := GenerateAndStore(ctx, store, func(_ crypto.PublicKey) (string, error) {
return "", errors.New("foo")
})

assert.ErrorContains(t, err, "foo")
})

t.Run("error - save public key returns an error", func(t *testing.T) {
ctrl := gomock.NewController(t)
store := NewMockStorage(ctrl)
store.EXPECT().PrivateKeyExists(ctx, "123").Return(false, nil)
store.EXPECT().SavePrivateKey(ctx, gomock.Any(), gomock.Any()).Return(errors.New("foo"))
kid := "123"

_, _, err := GenerateAndStore(ctx, store, func(_ crypto.PublicKey) (string, error) {
return kid, nil
})

assert.ErrorContains(t, err, "could not create new keypair: could not save private key: foo")
})

t.Run("error - ID already in use", func(t *testing.T) {
ctrl := gomock.NewController(t)
store := NewMockStorage(ctrl)
store.EXPECT().PrivateKeyExists(ctx, "123").Return(true, nil)
kid := "123"

_, _, err := GenerateAndStore(ctx, store, func(_ crypto.PublicKey) (string, error) {
return kid, nil
})

assert.ErrorContains(t, err, "key with the given ID already exists")
})
}
16 changes: 16 additions & 0 deletions crypto/storage/spi/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions crypto/storage/spi/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,14 @@ func (w wrapper) DeletePrivateKey(ctx context.Context, kid string) error {
func (w wrapper) ListPrivateKeys(ctx context.Context) []string {
return w.wrappedBackend.ListPrivateKeys(ctx)
}

func (w wrapper) NewPrivateKey(ctx context.Context, namingFunc func(crypto.PublicKey) (string, error)) (crypto.PublicKey, string, error) {
publicKey, kid, err := w.wrappedBackend.NewPrivateKey(ctx, namingFunc)
if err != nil {
return nil, "", err
}
if err := w.validateKID(kid); err != nil {
return nil, "", err
}
return publicKey, kid, err
}
4 changes: 4 additions & 0 deletions crypto/storage/vault/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func NewVaultKVStorage(config Config) (spi.Storage, error) {
return vaultStorage, nil
}

func (v vaultKVStorage) NewPrivateKey(ctx context.Context, namingFunc func(crypto.PublicKey) (string, error)) (crypto.PublicKey, string, error) {
return spi.GenerateAndStore(ctx, v, namingFunc)
}

func configureVaultClient(cfg Config) (*vault.Client, error) {
vaultConfig := vault.DefaultConfig()
vaultConfig.Timeout = cfg.Timeout
Expand Down
Loading
Loading