Skip to content
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

Keymanager 'disk' implementation #167

Merged
Merged
Show file tree
Hide file tree
Changes from 10 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
10 changes: 8 additions & 2 deletions conf/server/server.conf
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,11 @@ providers {
}

# KeyManager "memory": A key manager for generating keys and signing certificates that stores keys in memory.
KeyManager "memory" {}
}
# KeyManager "memory" {}

# KeyManager "disk": A key manager for generating keys that stores keys on disk.
KeyManager "disk" {
# keys_file_path: Path to the file where the key manager will store keys.
keys_file_path = "./keys.json"
}
}
2 changes: 1 addition & 1 deletion pkg/common/jwt/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var (

func Setup(t *testing.T) (*JWTCA, *DefaultJWTValidator) {
ctx := context.Background()
km := keymanager.New(&keymanager.Config{})
km := keymanager.NewMemoryKeyManager(nil)
config := ValidatorConfig{
KeyManager: km,
ExpectedAudience: expAud,
Expand Down
23 changes: 12 additions & 11 deletions pkg/common/keymanager/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
"github.com/HewlettPackard/galadriel/pkg/common/cryptoutil"
)

// Base is a base implementation of KeyManager that can be embedded into
// base implementation of KeyManager that can be embedded into
// other KeyManager implementations (e.g. memory and disk).
type Base struct {
mu sync.RWMutex
type base struct {
mu *sync.RWMutex

generator Generator
entries map[string]*KeyEntry
Expand All @@ -28,21 +28,22 @@ type KeyEntry struct {
id string
}

// Config is the configuration for a Base KeyManager.
// Config is the configuration for a base KeyManager.
type Config struct {
// Optional Key Generator
Generator Generator
}

// New creates a new Base KeyManager.
func New(config *Config) *Base {
// newBase creates a new base KeyManager.
func newBase(config *Config) *base {
if config == nil {
config = &Config{}
}
if config.Generator == nil {
config.Generator = &defaultGenerator{}
}
return &Base{
return &base{
mu: &sync.RWMutex{},
generator: config.Generator,
entries: make(map[string]*KeyEntry),
}
Expand All @@ -68,7 +69,7 @@ func (k *KeyEntry) Signer() crypto.Signer {
}

// GenerateKey creates a new key pair and stores it in the KeyManager.
func (b *Base) GenerateKey(ctx context.Context, keyID string, keyType cryptoutil.KeyType) (Key, error) {
func (b *base) GenerateKey(ctx context.Context, keyID string, keyType cryptoutil.KeyType) (Key, error) {
if keyID == "" {
return nil, errors.New("key id is required")
}
Expand All @@ -89,7 +90,7 @@ func (b *Base) GenerateKey(ctx context.Context, keyID string, keyType cryptoutil
return newEntry, nil
}

func (b *Base) GetKey(ctx context.Context, id string) (Key, error) {
func (b *base) GetKey(ctx context.Context, id string) (Key, error) {
b.mu.RLock()
defer b.mu.RUnlock()

Expand All @@ -101,7 +102,7 @@ func (b *Base) GetKey(ctx context.Context, id string) (Key, error) {
return entry, nil
}

func (b *Base) GetKeys(ctx context.Context) ([]Key, error) {
func (b *base) GetKeys(ctx context.Context) ([]Key, error) {
b.mu.RLock()
defer b.mu.RUnlock()

Expand All @@ -113,7 +114,7 @@ func (b *Base) GetKeys(ctx context.Context) ([]Key, error) {
return keys, nil
}

func (b *Base) generateKeyEntry(keyID string, keyType cryptoutil.KeyType) (*KeyEntry, error) {
func (b *base) generateKeyEntry(keyID string, keyType cryptoutil.KeyType) (*KeyEntry, error) {
var err error
var privateKey crypto.Signer

Expand Down
4 changes: 2 additions & 2 deletions pkg/common/keymanager/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
)

func setup() (*Base, context.Context) {
b := New(&Config{})
func setup() (*base, context.Context) {
b := newBase(&Config{})
ctx := context.Background()
return b, ctx
}
Expand Down
143 changes: 143 additions & 0 deletions pkg/common/keymanager/disk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package keymanager

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"os"

"github.com/HewlettPackard/galadriel/pkg/common/cryptoutil"
)

const keyFilePerm = 0600

// Disk extends the base KeyManager to store keys in disk.
type Disk struct {
base

keysFilePath string
}

// NewDiskKeyManager creates a new Disk that stores keys in disk.
func NewDiskKeyManager(generator Generator, keysFilePath string) (*Disk, error) {
c := &Config{
Generator: generator,
}
base := newBase(c)

diskKeyManager := &Disk{
base: *base,
keysFilePath: keysFilePath,
}

err := diskKeyManager.loadKeysFromDisk()
if err != nil {
return nil, err
}

return diskKeyManager, nil
}

// GenerateKey generates a new key and stores it in disk.
func (d *Disk) GenerateKey(ctx context.Context, keyID string, keyType cryptoutil.KeyType) (Key, error) {
key, err := d.base.GenerateKey(ctx, keyID, keyType)
if err != nil {
return nil, err
}

err = d.saveKeysToDisk()
if err != nil {
return nil, err
}

return key, nil
}

func (d *Disk) loadKeysFromDisk() error {
data, err := os.ReadFile(d.keysFilePath)
if err != nil {
return nil // No keys file exists, no error
}

keys := make(map[string]string)
if err := json.Unmarshal(data, &keys); err != nil {
return fmt.Errorf("failed to unmarshal keys from disk: %w", err)
}

d.mu.RLock()
defer d.mu.RUnlock()

for id, keyBytes := range keys {
signer, err := convertToSigner([]byte(keyBytes))
if err != nil {
return fmt.Errorf("failed to create key entry: %w", err)
}

d.entries[id] = &KeyEntry{
PrivateKey: signer,
PublicKey: signer.Public(),
id: id,
}
}

return nil
}

// SaveKeysToDisk saves the keys in the key manager to disk.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SaveKeysToDisk saves the keys in the key manager to disk.
// saveKeysToDisk saves the keys in the key manager to disk.

func (d *Disk) saveKeysToDisk() error {
d.mu.Lock()
defer d.mu.Unlock()

keys := make(map[string]string)
for id, entry := range d.entries {
keyBytes, err := x509.MarshalPKCS8PrivateKey(entry.PrivateKey)
if err != nil {
return fmt.Errorf("failed to marshal private key: %w", err)
}

// Encode PEM block as string
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: keyBytes,
})
keys[id] = string(keyPEM)
}

data, err := json.Marshal(keys)
if err != nil {
return fmt.Errorf("failed to serialize keys: %w", err)
}

if err := os.WriteFile(d.keysFilePath, data, keyFilePerm); err != nil {
return fmt.Errorf("failed to write keys to disk: %w", err)
}

return nil
}

func convertToSigner(keyBytes []byte) (crypto.Signer, error) {
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("failed to decode PEM block containing private key")
}

key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}

switch key := key.(type) {
case *rsa.PrivateKey:
return key, nil
case *ecdsa.PrivateKey:
return key, nil
default:
return nil, errors.New("unsupported private key type")
}
}
57 changes: 57 additions & 0 deletions pkg/common/keymanager/disk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package keymanager

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/HewlettPackard/galadriel/pkg/common/cryptoutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDiskKeyManager(t *testing.T) {
tempDir, err := os.MkdirTemp("", "keymanager_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

dataDir := filepath.Join(tempDir, "keys-test.json")

// Create a new Disk key manager
keyManager, err := NewDiskKeyManager(nil, dataDir)
require.NoError(t, err)

// Generate a new key pair
keyID := "key1"
keyType := cryptoutil.RSA2048
key, err := keyManager.GenerateKey(context.Background(), keyID, keyType)
require.NoError(t, err)
require.NotNil(t, key)

// Get the generated key
gotKey, err := keyManager.GetKey(context.Background(), keyID)
require.NoError(t, err)
require.NotNil(t, gotKey)

// Verify the generated key's ID and type
assert.Equal(t, keyID, gotKey.ID())

// Verify the generated key's
assert.NotNil(t, gotKey.Signer())

// Load the Disk key manager from disk
loadedKeyManager, err := NewDiskKeyManager(nil, dataDir)
require.NoError(t, err)

// Get the loaded key
loadedKey, err := loadedKeyManager.GetKey(context.Background(), keyID)
require.NoError(t, err)
require.NotNil(t, loadedKey)

// Verify the loaded key's ID and type
assert.Equal(t, keyID, loadedKey.ID())

// Verify the loaded key's
assert.NotNil(t, loadedKey.Signer())
}
4 changes: 2 additions & 2 deletions pkg/common/keymanager/keymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/HewlettPackard/galadriel/pkg/common/cryptoutil"
)

// KeyManager provides a common interface for managing keys.
// Memory provides a common interface for managing keys.
type KeyManager interface {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change interface name to match the new comment ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the comment is wrong.

// GenerateKey generates a new key with the given ID and key type.
// If a key with that ID already exists, it is overwritten.
Expand All @@ -18,7 +18,7 @@ type KeyManager interface {
// an error is returned.
GetKey(ctx context.Context, id string) (Key, error)

// GetKeys returns all keys managed by the KeyManager.
// GetKeys returns all keys managed by the Memory.
GetKeys(ctx context.Context) ([]Key, error)
}

Expand Down
14 changes: 14 additions & 0 deletions pkg/common/keymanager/memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package keymanager

// Memory is a key manager that keeps keys in memory.
type Memory struct {
*base
}

func NewMemoryKeyManager(generator Generator) *Memory {
return &Memory{
base: newBase(&Config{
Generator: generator,
}),
}
}
19 changes: 0 additions & 19 deletions pkg/common/keymanager/memory/keymanager.go

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package memory
package keymanager

import (
"context"
Expand All @@ -9,12 +9,12 @@ import (
)

func TestNew(t *testing.T) {
km := New(nil)
km := newBase(nil)
assert.NotNil(t, km)
}

func TestKeyManager(t *testing.T) {
km := New(nil)
km := newBase(nil)
ctx := context.Background()

key1, err := km.GenerateKey(ctx, "foo", cryptoutil.RSA2048)
Expand Down
Loading