|
1 | 1 | [](https://pkg.go.dev/github.com/smartcontractkit/chainlink-common/keystore) |
2 | 2 |
|
3 | | -WARNING: In development do not use in production. |
4 | | - |
5 | | -# Keystore |
6 | | -Design principles: |
7 | | -- Use structs for typed extensibility of the interfaces. Easy |
8 | | -to wrap via a network layer if needed. |
9 | | -- Storage abstract. Keystore interfaces can be implemented with memory, file, database, etc. for storage to be useable in a variety of |
10 | | -contexts. Use write through caching to maintain synchronization between in memory keys and stored keys. |
11 | | -- Only the Admin interface mutates the keystore, all other interfaces are read only. Admin interface |
12 | | -is plural/batched to support atomic batched mutations. |
13 | | -- 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 |
14 | | -the context in which they wish to use it. |
15 | | -- Common serialization/encryption for all storage types. Protobuf serialization (compact, versioned) for key material and then key material encrypted before persistence with a passphase. |
16 | | - |
17 | | -Notes |
18 | | -- 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. |
| 3 | + |
| 4 | +## Keystore |
| 5 | + |
| 6 | +Dependency minimized chain family agnostic key storage library. Supports the following key types: |
| 7 | +- Digital Signatures: ECDSA on secp256k1 and Ed25519. |
| 8 | +Note KMS is supported for these key types. |
| 9 | +- Hybrid Encryption: X25519 (nacl/box) and ECDH on P256 |
| 10 | + |
| 11 | +Warning: ECDH on P256 is pending audit do not use in |
| 12 | +production. |
| 13 | + |
| 14 | +Family specific logic is layered on top in the following locations: |
| 15 | +- [EVM](https://github.com/smartcontractkit/chainlink-evm/tree/develop/pkg/keys/v2) |
| 16 | + - Note this also holds a full e2e example of using the keystore with [libocr](https://github.com/smartcontractkit/libocr/tree/master). |
| 17 | +- [Solana](https://github.com/smartcontractkit/chainlink-solana/tree/develop/pkg/solana/keys) |
| 18 | +- More coming soon |
| 19 | + |
| 20 | + |
| 21 | +### Examples |
| 22 | + |
| 23 | +#### Signatures |
| 24 | +```go |
| 25 | +package main |
| 26 | + |
| 27 | +import ( |
| 28 | + "context" |
| 29 | + "crypto/sha256" |
| 30 | + |
| 31 | + "github.com/smartcontractkit/chainlink-common/keystore" |
| 32 | +) |
| 33 | + |
| 34 | +func main() { |
| 35 | + ctx := context.Background() |
| 36 | + |
| 37 | + // Note postgres and file based storage also supported. |
| 38 | + ks, err := keystore.LoadKeystore(ctx, keystore.NewMemoryStorage(), "password") |
| 39 | + |
| 40 | + createResp, _ := ks.CreateKeys(ctx, keystore.CreateKeysRequest{ |
| 41 | + Keys: []keystore.CreateKeyRequest{ |
| 42 | + {KeyName: "my-key", KeyType: keystore.ECDSA_S256}, |
| 43 | + }, |
| 44 | + }) |
| 45 | + // Sign data |
| 46 | + data := []byte("hello world") |
| 47 | + hash := sha256.Sum256(data) |
| 48 | + |
| 49 | + signResp, _ := ks.Sign(ctx, keystore.SignRequest{ |
| 50 | + KeyName: "my-key", |
| 51 | + Data: hash[:], |
| 52 | + }) |
| 53 | + |
| 54 | + // Verify the signature |
| 55 | + verifyResp, _ := ks.Verify(ctx, keystore.VerifyRequest{ |
| 56 | + KeyType: keystore.ECDSA_S256, |
| 57 | + PublicKey: createResp.Keys[0].KeyInfo.PublicKey, |
| 58 | + Data: hash[:], |
| 59 | + Signature: signResp.Signature, |
| 60 | + }) |
| 61 | + // verifyResp.Valid == true |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +#### KMS Signatures |
| 66 | +```go |
| 67 | +package main |
| 68 | + |
| 69 | +import ( |
| 70 | + "context" |
| 71 | + "crypto/sha256" |
| 72 | + |
| 73 | + "github.com/smartcontractkit/chainlink-common/keystore" |
| 74 | + "github.com/smartcontractkit/chainlink-common/keystore/kms" |
| 75 | +) |
| 76 | + |
| 77 | +func main() { |
| 78 | + ctx := context.Background() |
| 79 | + |
| 80 | + // Create a KMS backed keystore. |
| 81 | + kmsClient, _ := kms.NewClient(ctx, kms.ClientOptions{ |
| 82 | + Profile: "my-profile", // Optional: omit for default credential chain |
| 83 | + }) |
| 84 | + ks, _ := kms.NewKeystore(kmsClient) |
| 85 | + |
| 86 | + data := []byte("hello world") |
| 87 | + hash := sha256.Sum256(data) |
| 88 | + // Same signer interface but uses AWS allocated |
| 89 | + // key names. |
| 90 | + signResp, _ := ks.Sign(ctx, keystore.SignRequest{ |
| 91 | + KeyName: "AWSkeyID", |
| 92 | + Data: hash[:], |
| 93 | + }) |
| 94 | + |
| 95 | + // Verify the signature |
| 96 | + verifyResp, _ := keystore.Verify(ctx, keystore.VerifyRequest{ |
| 97 | + KeyType: keysResp.Keys[0].KeyInfo.KeyType, |
| 98 | + PublicKey: keysResp.Keys[0].KeyInfo.PublicKey, |
| 99 | + Data: hash[:], |
| 100 | + Signature: signResp.Signature, |
| 101 | + }) |
| 102 | + // verifyResp.Valid == true |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | + |
| 107 | +#### Encryption |
| 108 | +```go |
| 109 | +package main |
| 110 | + |
| 111 | +import ( |
| 112 | + "context" |
| 113 | + |
| 114 | + "github.com/smartcontractkit/chainlink-common/keystore" |
| 115 | +) |
| 116 | + |
| 117 | +func main() { |
| 118 | + ctx := context.Background() |
| 119 | + |
| 120 | + ks, _ := keystore.LoadKeystore(ctx, keystore.NewMemoryStorage(), "password") |
| 121 | + |
| 122 | + // Create an X25519 key for encryption/decryption |
| 123 | + createResp, _ := ks.CreateKeys(ctx, keystore.CreateKeysRequest{ |
| 124 | + Keys: []keystore.CreateKeyRequest{ |
| 125 | + {KeyName: "encrypt-key", KeyType: keystore.X25519}, |
| 126 | + }, |
| 127 | + }) |
| 128 | + |
| 129 | + // Encrypt data using the public key |
| 130 | + data := []byte("secret message") |
| 131 | + encryptResp, _ := ks.Encrypt(ctx, keystore.EncryptRequest{ |
| 132 | + RemoteKeyType: keystore.X25519, |
| 133 | + RemotePubKey: createResp.Keys[0].KeyInfo.PublicKey, |
| 134 | + Data: data, |
| 135 | + }) |
| 136 | + |
| 137 | + // Decrypt using the key name |
| 138 | + decryptResp, _ := ks.Decrypt(ctx, keystore.DecryptRequest{ |
| 139 | + KeyName: "encrypt-key", |
| 140 | + EncryptedData: encryptResp.EncryptedData, |
| 141 | + }) |
| 142 | + // decryptResp.Data == data |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | + |
| 147 | +#### CLI |
| 148 | +```bash |
| 149 | +# Set up environment variables |
| 150 | +export KEYSTORE_PASSWORD="my-secure-password" |
| 151 | +export KEYSTORE_FILE_PATH="./keystore.json" |
| 152 | + |
| 153 | +# Create a new keystore file (if using file storage) |
| 154 | +touch ./keystore.json |
| 155 | + |
| 156 | +# Create an ECDSA key |
| 157 | +keys create -d '{"Keys": [{"KeyName": "my-key", "KeyType": "ECDSA_S256"}]}' |
| 158 | + |
| 159 | +# List all keys |
| 160 | +keys list |
| 161 | + |
| 162 | +# Get a specific key |
| 163 | +keys get -d '{"KeyNames": ["my-key"]}' |
| 164 | + |
| 165 | +# Sign data (data must be base64-encoded, 32 bytes for ECDSA_S256) |
| 166 | +echo -n "hello world" | shasum -a 256 | cut -d' ' -f1 | xxd -r -p | base64 |
| 167 | +# Use the output in the sign command: |
| 168 | +keys sign -d '{"KeyName": "my-key", "Data": "<base64-hash>"}' |
| 169 | + |
| 170 | +# Verify a signature |
| 171 | +keys verify -d '{"KeyType": "ECDSA_S256", "PublicKey": "<base64-public-key>", "Data": "<base64-hash>", "Signature": "<base64-signature>"}' |
| 172 | +``` |
| 173 | + |
| 174 | +For KMS usage, set `KEYSTORE_KMS_PROFILE` instead: |
| 175 | +```bash |
| 176 | +export KEYSTORE_KMS_PROFILE="my-aws-profile" |
| 177 | +keys list # Lists KMS keys |
| 178 | +keys sign -d '{"KeyName": "arn:aws:kms:us-west-2:123456789012:key/abc123", "Data": "<base64-hash>"}' |
| 179 | +``` |
| 180 | + |
| 181 | +### Design Principles |
| 182 | +- **Embeddable CLI** The cli package is designed to support |
| 183 | +embedding in downstream applications so a consistent CLI |
| 184 | +can be shared across them. |
| 185 | +- **Typed extensibility**: Use structs for requests/responses that are extensible and easy to wrap via a network layer if needed. |
| 186 | +- **Storage abstract**: Keystore interfaces can be implemented with memory, file, database, etc. for storage to be useable in a variety of contexts. Uses write-through caching to maintain synchronization between in-memory keys and stored keys. |
| 187 | +- **Admin interface for mutations**: Only the Admin interface mutates the keystore; all other interfaces are read-only. Admin interface is plural/batched to support atomic batched mutations. |
| 188 | +- **Client side key naming**: Keystore doesn't impose specific key algorithms/curves for specific contexts. It supports a minimum viable set of algorithms/curves for chainlink-wide use cases. Clients define a name for each key representing the context in which they wish to use it. |
| 189 | +- **Common serialization/encryption**: Protobuf serialization (compact, versioned) for key material, then encrypted before persistence with a passphrase. |
0 commit comments