-
Notifications
You must be signed in to change notification settings - Fork 25
Keystore library interfaces and admin create/delete #1580
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
connorwstein
merged 19 commits into
main
from
ARCH-328-keystore-interfaces-and-admin-implementation
Oct 6, 2025
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
7884a66
Interfaces and part of admin impl
connorwstein 3f29c6a
Port test for raw
connorwstein a2f2d18
Restructure a bit
connorwstein c1f611c
Basic test working
connorwstein 647b07c
Basic tests
connorwstein a1509b3
Fix spacing
connorwstein 8ecc38c
Simpler test
connorwstein a528270
Concurrecy test and some smaller fixes
connorwstein e223a82
Merge branch 'main' into ARCH-328-keystore-interfaces-and-admin-imple…
connorwstein b74b144
Small test for secp pubkey
connorwstein f22fc6f
Merge branch 'ARCH-328-keystore-interfaces-and-admin-implementation' …
connorwstein 7683283
Merge branch 'main' into ARCH-328-keystore-interfaces-and-admin-imple…
connorwstein e69b870
Merge branch 'ARCH-328-keystore-interfaces-and-admin-implementation' …
connorwstein d807ea7
PR comments
connorwstein 2b9df50
Rename to LoadKeystore
connorwstein 6e77ff5
Merge branch 'main' into ARCH-328-keystore-interfaces-and-admin-imple…
connorwstein 885c468
Encryptor documentation
connorwstein e820fc8
Merge branch 'ARCH-328-keystore-interfaces-and-admin-implementation' …
connorwstein d28facc
Allow external impls
connorwstein File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.