Skip to content
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
14 changes: 14 additions & 0 deletions keystore/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Keystore
Design principles:
- Use structs for typed extensibility of the interfaces. Easy
to wrap via a network layer if needed.
- Storage abstract. Keystore interfaces can be implemented with memory, file, database, etc. for storage to be useable in a variety of
contexts. Use write through caching to maintain synchronization between in memory keys and stored keys.
- Only the Admin interface mutates the keystore, all other interfaces are read only. Admin interface
is plural/batched to support atomic batched mutations.
- Client side key naming. Keystore itself doesn't impose certain key algorithims/curves be used for specific contexts, it just supports a the minimum viable set of algorithms/curves for chainlink wide use cases. Clients define a name for each key which represents
the context in which they wish to use it.
- Common serialization/encryption for all storage types. Protobuf serialization (compact, versioned) for key material and then key material encrypted before persistence with a passphase.

Notes
- keystore/internal is copied from https://github.com/smartcontractkit/chainlink/blob/develop/core/services/keystore/internal/raw.go#L3. Intention is to switch to core to use this library at which point we can remove the core copy.
224 changes: 224 additions & 0 deletions keystore/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package keystore

import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"fmt"
"maps"
"time"

"golang.org/x/crypto/curve25519"

"github.com/ethereum/go-ethereum/crypto"
"github.com/smartcontractkit/chainlink-common/keystore/internal"
)

var (
ErrKeyAlreadyExists = fmt.Errorf("key already exists")
ErrInvalidKeyName = fmt.Errorf("invalid key name")
ErrKeyNotFound = fmt.Errorf("key not found")
ErrUnsupportedKeyType = fmt.Errorf("unsupported key type")
)

type CreateKeysRequest struct {
Keys []CreateKeyRequest
}

type CreateKeyRequest struct {
KeyName string
KeyType KeyType
}

type CreateKeysResponse struct {
Keys []CreateKeyResponse
}

type CreateKeyResponse struct {
KeyInfo KeyInfo
}

type DeleteKeysRequest struct {
KeyNames []string
}

type DeleteKeysResponse struct{}

type ImportKeysRequest struct {
Keys []ImportKeyRequest
}

type ImportKeyRequest struct {
KeyName string
KeyType KeyType
Data []byte
}

type ImportKeysResponse struct{}

type ExportKeyParam struct {
KeyName string
Enc EncryptionParams
}

type ExportKeysRequest struct {
Keys []ExportKeyParam
}

type ExportKeysResponse struct {
Keys []ExportKeyResponse
}

type ExportKeyResponse struct {
KeyName string
Data []byte
}

type SetMetadataRequest struct {
Updates []SetMetadataUpdate
}

type SetMetadataUpdate struct {
KeyName string
Metadata []byte
}

type SetMetadataResponse struct{}

type Admin interface {
CreateKeys(ctx context.Context, req CreateKeysRequest) (CreateKeysResponse, error)
DeleteKeys(ctx context.Context, req DeleteKeysRequest) (DeleteKeysResponse, error)
ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error)
ExportKeys(ctx context.Context, req ExportKeysRequest) (ExportKeysResponse, error)
SetMetadata(ctx context.Context, req SetMetadataRequest) (SetMetadataResponse, error)
}

// UnimplementedAdmin returns ErrUnimplemented for all Admin methods.
type UnimplementedAdmin struct{}

func (UnimplementedAdmin) CreateKeys(ctx context.Context, req CreateKeysRequest) (CreateKeysResponse, error) {
return CreateKeysResponse{}, fmt.Errorf("Admin.CreateKeys: %w", ErrUnimplemented)
}

func (UnimplementedAdmin) DeleteKeys(ctx context.Context, req DeleteKeysRequest) (DeleteKeysResponse, error) {
return DeleteKeysResponse{}, fmt.Errorf("Admin.DeleteKeys: %w", ErrUnimplemented)
}

func (UnimplementedAdmin) ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error) {
return ImportKeysResponse{}, fmt.Errorf("Admin.ImportKeys: %w", ErrUnimplemented)
}

func (UnimplementedAdmin) ExportKeys(ctx context.Context, req ExportKeysRequest) (ExportKeysResponse, error) {
return ExportKeysResponse{}, fmt.Errorf("Admin.ExportKeys: %w", ErrUnimplemented)
}

func (UnimplementedAdmin) SetMetadata(ctx context.Context, req SetMetadataRequest) (SetMetadataResponse, error) {
return SetMetadataResponse{}, fmt.Errorf("Admin.SetMetadata: %w", ErrUnimplemented)
}

func ValidKeyName(name string) error {
if name == "" {
return fmt.Errorf("key name cannot be empty")
}
// Just a sanity bound.
if len(name) > 1_000 {
return fmt.Errorf("key name cannot be longer than 1000 characters")
}
return nil
}

func (ks *keystore) CreateKeys(ctx context.Context, req CreateKeysRequest) (CreateKeysResponse, error) {
ks.mu.Lock()
defer ks.mu.Unlock()

ksCopy := maps.Clone(ks.keystore)
var responses []CreateKeyResponse
for _, keyReq := range req.Keys {
if _, ok := ksCopy[keyReq.KeyName]; ok {
return CreateKeysResponse{}, fmt.Errorf("%w: %s", ErrKeyAlreadyExists, keyReq.KeyName)
}
if err := ValidKeyName(keyReq.KeyName); err != nil {
return CreateKeysResponse{}, fmt.Errorf("%w: %s", ErrInvalidKeyName, err)
}
switch keyReq.KeyType {
case Ed25519:
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return CreateKeysResponse{}, fmt.Errorf("failed to generate Ed25519 key: %w", err)
}
publicKey, err := publicKeyFromPrivateKey(internal.NewRaw(privateKey), keyReq.KeyType)
if err != nil {
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
}
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey), publicKey, time.Now(), []byte{})
case EcdsaSecp256k1:
privateKey, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
if err != nil {
return CreateKeysResponse{}, fmt.Errorf("failed to generate EcdsaSecp256k1 key: %w", err)
}
publicKey, err := publicKeyFromPrivateKey(internal.NewRaw(privateKey.D.Bytes()), keyReq.KeyType)
if err != nil {
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
}
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey.D.Bytes()), publicKey, time.Now(), []byte{})
case X25519:
privateKey := [curve25519.ScalarSize]byte{}
_, err := rand.Read(privateKey[:])
if err != nil {
return CreateKeysResponse{}, fmt.Errorf("failed to generate Curve25519 key: %w", err)
}
publicKey, err := publicKeyFromPrivateKey(internal.NewRaw(privateKey[:]), keyReq.KeyType)
if err != nil {
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
}
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey[:]), publicKey, time.Now(), []byte{})
default:
return CreateKeysResponse{}, fmt.Errorf("%w: %s", ErrUnsupportedKeyType, keyReq.KeyType)
}

created := ksCopy[keyReq.KeyName].createdAt
k := ksCopy[keyReq.KeyName]
responses = append(responses, CreateKeyResponse{
KeyInfo: newKeyInfo(keyReq.KeyName, keyReq.KeyType, created, k.publicKey, k.metadata),
})
}

// Persist it to storage.
if err := ks.save(ctx, ksCopy); err != nil {
return CreateKeysResponse{}, fmt.Errorf("failed to save keystore: %w", err)
}
// If we succeed to save, update the in memory keystore.
ks.keystore = ksCopy
return CreateKeysResponse{Keys: responses}, nil
}

func (k *keystore) DeleteKeys(ctx context.Context, req DeleteKeysRequest) (DeleteKeysResponse, error) {
k.mu.Lock()
defer k.mu.Unlock()

ksCopy := maps.Clone(k.keystore)
for _, name := range req.KeyNames {
if _, ok := ksCopy[name]; !ok {
return DeleteKeysResponse{}, fmt.Errorf("%w: %s", ErrKeyNotFound, name)
}
delete(ksCopy, name)
}
if err := k.save(ctx, ksCopy); err != nil {
return DeleteKeysResponse{}, fmt.Errorf("failed to save keystore: %w", err)
}
k.keystore = ksCopy
return DeleteKeysResponse{}, nil
}

func (k *keystore) ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error) {
return ImportKeysResponse{}, nil
}

func (k *keystore) ExportKeys(ctx context.Context, req ExportKeysRequest) (ExportKeysResponse, error) {
return ExportKeysResponse{}, nil
}

func (ks *keystore) SetMetadata(ctx context.Context, req SetMetadataRequest) (SetMetadataResponse, error) {
return SetMetadataResponse{}, nil
}
Loading
Loading