@@ -12,10 +12,11 @@ import (
1212 "time"
1313
1414 "github.com/jmoiron/sqlx"
15- _ "github.com/lib/pq"
15+ _ "github.com/lib/pq" // Register postgres driver
1616 "github.com/spf13/cobra"
1717
1818 ks "github.com/smartcontractkit/chainlink-common/keystore"
19+ "github.com/smartcontractkit/chainlink-common/keystore/kms"
1920 "github.com/smartcontractkit/chainlink-common/keystore/pgstore"
2021)
2122
@@ -27,12 +28,15 @@ func NewRootCmd() *cobra.Command {
2728 cmd := & cobra.Command {
2829 Use : "keys" ,
2930 Long : `
30- CLI for managing keystore keys. Must specify KEYSTORE_FILE_PATH or KEYSTORE_DB_URL
31- and KEYSTORE_PASSWORD in order to load the keystore.
31+ CLI for managing keystore keys.
3232
33- KEYSTORE_FILE_PATH: is the path to the keystore file, can be empty for a new keystore .
34- File must already exist. Example to create a new keystore file: touch ./keystore.json
33+ If KEYSTORE_KMS_PROFILE is set, will load the keystore from KMS .
34+ KEYSTORE_KMS_PROFILE: is the AWS profile to use for KMS (region will be taken from the profile).
3535
36+ Otherwise, will load the keystore from a file or database.
37+ KEYSTORE_PASSWORD: password used to encrypt the key material before storage, must be provided.
38+ KEYSTORE_FILE_PATH: is the path to the keystore file, can be empty for a new keystore.
39+ File must already exist. Example to create a new keystore file: touch ./keystore.json. Either KEYSTORE_FILE_PATH or KEYSTORE_DB_URL must be set.
3640KEYSTORE_DB_URL: is the postgres connection URL. Only use this if your keystore is stored in a pg database.
3741Requires a pg database with a 'encrypted_keystore' table with the following schema:
3842CREATE TABLE IF NOT EXISTS encrypted_keystore (
@@ -42,17 +46,44 @@ CREATE TABLE IF NOT EXISTS encrypted_keystore (
4246 updated_at timestamptz NOT NULL DEFAULT NOW(),
4347 encrypted_data BYTEA NOT NULL
4448);
45-
46- KEYSTORE_PASSWORD is the password used to encrypt the key material before storage.
4749` ,
4850 Short : "CLI for managing keystore keys" ,
4951 SilenceUsage : true ,
5052 }
51- cmd .PersistentFlags ().String ("file-path" , "" , "Overrides KEYSTORE_FILE_PATH environment variable" )
52- cmd .PersistentFlags ().String ("db-url" , "" , "Overrides KEYSTORE_DB_URL environment variable" )
53- cmd .PersistentFlags ().String ("password" , "" , "Overrides KEYSTORE_PASSWORD environment variable. Not recommended as will leave shell traces." )
5453
55- cmd .AddCommand (NewListCmd (), NewGetCmd (), NewCreateCmd (), NewDeleteCmd (), NewExportCmd (), NewImportCmd (), NewSetMetadataCmd (), NewSignCmd (), NewVerifyCmd (), NewEncryptCmd (), NewDecryptCmd ())
54+ // Check if KMS profile is set - if so, hide commands that don't work with KMS
55+ isKMSMode := os .Getenv ("KEYSTORE_KMS_PROFILE" ) != ""
56+
57+ // Commands that work with both regular keystore and KMS
58+ listCmd := NewListCmd ()
59+ getCmd := NewGetCmd ()
60+ signCmd := NewSignCmd ()
61+ verifyCmd := NewVerifyCmd ()
62+
63+ // Commands that only work with regular keystore (not KMS)
64+ createCmd := NewCreateCmd ()
65+ deleteCmd := NewDeleteCmd ()
66+ exportCmd := NewExportCmd ()
67+ importCmd := NewImportCmd ()
68+ setMetadataCmd := NewSetMetadataCmd ()
69+ renameCmd := NewRenameCmd ()
70+ // Note these could potentially be supported with KMS, but not yet implemented.
71+ encryptCmd := NewEncryptCmd ()
72+ decryptCmd := NewDecryptCmd ()
73+
74+ // Hide admin/encryption commands when using KMS (keys are managed externally)
75+ if isKMSMode {
76+ createCmd .Hidden = true
77+ deleteCmd .Hidden = true
78+ exportCmd .Hidden = true
79+ importCmd .Hidden = true
80+ setMetadataCmd .Hidden = true
81+ renameCmd .Hidden = true
82+ encryptCmd .Hidden = true
83+ decryptCmd .Hidden = true
84+ }
85+
86+ cmd .AddCommand (listCmd , getCmd , createCmd , deleteCmd , exportCmd , importCmd , setMetadataCmd , renameCmd , signCmd , verifyCmd , encryptCmd , decryptCmd )
5687 return cmd
5788}
5889
@@ -62,7 +93,7 @@ func NewListCmd() *cobra.Command {
6293 RunE : func (cmd * cobra.Command , args []string ) error {
6394 ctx , cancel := context .WithTimeout (cmd .Context (), KeystoreLoadTimeout )
6495 defer cancel ()
65- k , err := loadKeystore (ctx , cmd )
96+ k , err := loadKeystoreSignerReader (ctx , cmd )
6697 if err != nil {
6798 return err
6899 }
@@ -85,7 +116,13 @@ func NewGetCmd() *cobra.Command {
85116 cmd := cobra.Command {
86117 Use : "get" , Short : "Get keys" ,
87118 RunE : func (cmd * cobra.Command , args []string ) error {
88- return runKeystoreCommand [ks.GetKeysRequest , ks.GetKeysResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.GetKeysRequest ) (ks.GetKeysResponse , error ) {
119+ return runKeystoreCommand [interface {
120+ ks.Reader
121+ ks.Signer
122+ }, ks.GetKeysRequest , ks.GetKeysResponse ](cmd , args , loadKeystoreSignerReader , func (ctx context.Context , k interface {
123+ ks.Reader
124+ ks.Signer
125+ }, req ks.GetKeysRequest ) (ks.GetKeysResponse , error ) {
89126 return k .GetKeys (ctx , req )
90127 })
91128 },
@@ -99,7 +136,7 @@ func NewCreateCmd() *cobra.Command {
99136 cmd := cobra.Command {
100137 Use : "create" , Short : "Create a key" ,
101138 RunE : func (cmd * cobra.Command , args []string ) error {
102- return runKeystoreCommand [ks.CreateKeysRequest , ks.CreateKeysResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.CreateKeysRequest ) (ks.CreateKeysResponse , error ) {
139+ return runKeystoreCommand [ks.Keystore , ks. CreateKeysRequest , ks.CreateKeysResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.CreateKeysRequest ) (ks.CreateKeysResponse , error ) {
103140 return k .CreateKeys (ctx , req )
104141 })
105142 },
@@ -128,7 +165,7 @@ func NewDeleteCmd() *cobra.Command {
128165 }
129166 if ! confirmYes {
130167 // Prompt for confirmation on stdin
131- _ , err = cmd .OutOrStderr (). Write ([] byte ( fmt . Sprintf ( "This will permanently delete keys: %v. Type 'yes' to confirm: " , strings .Join (req .KeyNames , ", " )) ))
168+ _ , err = fmt . Fprintf ( cmd .OutOrStderr (), "This will permanently delete keys: %v. Type 'yes' to confirm: " , strings .Join (req .KeyNames , ", " ))
132169 if err != nil {
133170 return err
134171 }
@@ -159,7 +196,7 @@ func NewExportCmd() *cobra.Command {
159196 cmd := cobra.Command {
160197 Use : "export" , Short : "Export a key to an encrypted JSON file" ,
161198 RunE : func (cmd * cobra.Command , args []string ) error {
162- return runKeystoreCommand [ks.ExportKeysRequest , ks.ExportKeysResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.ExportKeysRequest ) (ks.ExportKeysResponse , error ) {
199+ return runKeystoreCommand [ks.Keystore , ks. ExportKeysRequest , ks.ExportKeysResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.ExportKeysRequest ) (ks.ExportKeysResponse , error ) {
163200 return k .ExportKeys (ctx , req )
164201 })
165202 },
@@ -173,7 +210,7 @@ func NewImportCmd() *cobra.Command {
173210 cmd := cobra.Command {
174211 Use : "import" , Short : "Import an encrypted key JSON file" ,
175212 RunE : func (cmd * cobra.Command , args []string ) error {
176- return runKeystoreCommand [ks.ImportKeysRequest , ks.ImportKeysResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.ImportKeysRequest ) (ks.ImportKeysResponse , error ) {
213+ return runKeystoreCommand [ks.Keystore , ks. ImportKeysRequest , ks.ImportKeysResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.ImportKeysRequest ) (ks.ImportKeysResponse , error ) {
177214 return k .ImportKeys (ctx , req )
178215 })
179216 },
@@ -187,7 +224,7 @@ func NewSetMetadataCmd() *cobra.Command {
187224 cmd := cobra.Command {
188225 Use : "set-metadata" , Short : "Set metadata for keys" ,
189226 RunE : func (cmd * cobra.Command , args []string ) error {
190- return runKeystoreCommand [ks.SetMetadataRequest , ks.SetMetadataResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.SetMetadataRequest ) (ks.SetMetadataResponse , error ) {
227+ return runKeystoreCommand [ks.Keystore , ks. SetMetadataRequest , ks.SetMetadataResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.SetMetadataRequest ) (ks.SetMetadataResponse , error ) {
191228 return k .SetMetadata (ctx , req )
192229 })
193230 },
@@ -197,8 +234,27 @@ func NewSetMetadataCmd() *cobra.Command {
197234 return & cmd
198235}
199236
200- func runKeystoreCommand [Req any , Resp any ](cmd * cobra.Command , args []string , fn func (ctx context.Context , k ks.Keystore ,
201- req Req ) (Resp , error )) error {
237+ func NewRenameCmd () * cobra.Command {
238+ cmd := cobra.Command {
239+ Use : "rename" , Short : "Rename a key" ,
240+ RunE : func (cmd * cobra.Command , args []string ) error {
241+ return runKeystoreCommand [ks.Keystore , ks.RenameKeyRequest , ks.RenameKeyResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.RenameKeyRequest ) (ks.RenameKeyResponse , error ) {
242+ return k .RenameKey (ctx , req )
243+ })
244+ },
245+ }
246+ cmd .Flags ().StringP ("file" , "f" , "" , "input file path (use \" -\" for stdin)" )
247+ cmd .Flags ().StringP ("data" , "d" , "" , "inline JSON request, e.g. '{\" OldName\" : \" key1\" , \" NewName\" : \" key2\" }'" )
248+ return & cmd
249+ }
250+
251+ // runKeystoreCommandGeneric is a generic helper that runs a keystore command with a custom loader function.
252+ func runKeystoreCommand [K any , Req any , Resp any ](
253+ cmd * cobra.Command ,
254+ args []string ,
255+ loader func (ctx context.Context , cmd * cobra.Command ) (K , error ),
256+ fn func (ctx context.Context , k K , req Req ) (Resp , error ),
257+ ) error {
202258 jsonBytes , err := readJSONInput (cmd )
203259 if err != nil {
204260 return err
@@ -210,7 +266,7 @@ func runKeystoreCommand[Req any, Resp any](cmd *cobra.Command, args []string, fn
210266 }
211267 ctx , cancel := context .WithTimeout (cmd .Context (), KeystoreLoadTimeout )
212268 defer cancel ()
213- k , err := loadKeystore (ctx , cmd )
269+ k , err := loader (ctx , cmd )
214270 if err != nil {
215271 return err
216272 }
@@ -233,7 +289,13 @@ func NewSignCmd() *cobra.Command {
233289 cmd := cobra.Command {
234290 Use : "sign" , Short : "Sign data with a key" ,
235291 RunE : func (cmd * cobra.Command , args []string ) error {
236- return runKeystoreCommand [ks.SignRequest , ks.SignResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.SignRequest ) (ks.SignResponse , error ) {
292+ return runKeystoreCommand [interface {
293+ ks.Reader
294+ ks.Signer
295+ }, ks.SignRequest , ks.SignResponse ](cmd , args , loadKeystoreSignerReader , func (ctx context.Context , k interface {
296+ ks.Reader
297+ ks.Signer
298+ }, req ks.SignRequest ) (ks.SignResponse , error ) {
237299 return k .Sign (ctx , req )
238300 })
239301 },
@@ -272,7 +334,13 @@ func NewVerifyCmd() *cobra.Command {
272334 cmd := cobra.Command {
273335 Use : "verify" , Short : "Verify a signature" ,
274336 RunE : func (cmd * cobra.Command , args []string ) error {
275- return runKeystoreCommand [ks.VerifyRequest , ks.VerifyResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.VerifyRequest ) (ks.VerifyResponse , error ) {
337+ return runKeystoreCommand [interface {
338+ ks.Reader
339+ ks.Signer
340+ }, ks.VerifyRequest , ks.VerifyResponse ](cmd , args , loadKeystoreSignerReader , func (ctx context.Context , k interface {
341+ ks.Reader
342+ ks.Signer
343+ }, req ks.VerifyRequest ) (ks.VerifyResponse , error ) {
276344 return k .Verify (ctx , req )
277345 })
278346 },
@@ -286,7 +354,7 @@ func NewEncryptCmd() *cobra.Command {
286354 cmd := cobra.Command {
287355 Use : "encrypt" , Short : "Encrypt data to a remote public key" ,
288356 RunE : func (cmd * cobra.Command , args []string ) error {
289- return runKeystoreCommand [ks.EncryptRequest , ks.EncryptResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.EncryptRequest ) (ks.EncryptResponse , error ) {
357+ return runKeystoreCommand [ks.Keystore , ks. EncryptRequest , ks.EncryptResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.EncryptRequest ) (ks.EncryptResponse , error ) {
290358 return k .Encrypt (ctx , req )
291359 })
292360 },
@@ -325,7 +393,7 @@ func NewDecryptCmd() *cobra.Command {
325393 cmd := cobra.Command {
326394 Use : "decrypt" , Short : "Decrypt data with a key" ,
327395 RunE : func (cmd * cobra.Command , args []string ) error {
328- return runKeystoreCommand [ks.DecryptRequest , ks.DecryptResponse ](cmd , args , func (ctx context.Context , k ks.Keystore , req ks.DecryptRequest ) (ks.DecryptResponse , error ) {
396+ return runKeystoreCommand [ks.Keystore , ks. DecryptRequest , ks.DecryptResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.DecryptRequest ) (ks.DecryptResponse , error ) {
329397 return k .Decrypt (ctx , req )
330398 })
331399 },
@@ -335,38 +403,27 @@ func NewDecryptCmd() *cobra.Command {
335403 return & cmd
336404}
337405
338- func loadKeystore (ctx context.Context , cmd * cobra.Command ) (ks.Keystore , error ) {
339- // Use parent command which has the persistent flags.
340- // This works whether keystore CLI is standalone or embedded as a subcommand.
341- parent := cmd .Parent ()
342- filePath , err := parent .Flags ().GetString ("file-path" )
343- if err != nil {
344- return nil , err
345- }
346- dbURL , err := parent .Flags ().GetString ("db-url" )
347- if err != nil {
348- return nil , err
349- }
350- password , err := parent .Flags ().GetString ("password" )
351- if err != nil {
352- return nil , err
353- }
354- // Env var fallbacks (flags override)
355- if filePath == "" {
356- if v := os .Getenv ("KEYSTORE_FILE_PATH" ); v != "" {
357- filePath = v
358- }
359- }
360- if dbURL == "" {
361- if v := os .Getenv ("KEYSTORE_DB_URL" ); v != "" {
362- dbURL = v
363- }
364- }
365- if password == "" {
366- if v := os .Getenv ("KEYSTORE_PASSWORD" ); v != "" {
367- password = v
406+ func loadKeystoreSignerReader (ctx context.Context , cmd * cobra.Command ) (interface {
407+ ks.Reader
408+ ks.Signer
409+ }, error ) {
410+ // Check if KMS mode is enabled
411+ kmsProfile := os .Getenv ("KEYSTORE_KMS_PROFILE" )
412+ if kmsProfile != "" {
413+ client , err := kms .NewClient (kmsProfile )
414+ if err != nil {
415+ return nil , fmt .Errorf ("create KMS client: %w" , err )
368416 }
417+ return kms .NewKeystore (client )
369418 }
419+ return loadKeystore (ctx , cmd )
420+ }
421+
422+ func loadKeystore (ctx context.Context , cmd * cobra.Command ) (ks.Keystore , error ) {
423+ // Read from environment variables only
424+ filePath := os .Getenv ("KEYSTORE_FILE_PATH" )
425+ dbURL := os .Getenv ("KEYSTORE_DB_URL" )
426+ password := os .Getenv ("KEYSTORE_PASSWORD" )
370427 if password == "" {
371428 return nil , errors .New ("keystore password is required" )
372429 }
0 commit comments