@@ -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,42 @@ 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+ // Note these could potentially be supported with KMS, but not yet implemented.
70+ encryptCmd := NewEncryptCmd ()
71+ decryptCmd := NewDecryptCmd ()
72+
73+ // Hide admin/encryption commands when using KMS (keys are managed externally)
74+ if isKMSMode {
75+ createCmd .Hidden = true
76+ deleteCmd .Hidden = true
77+ exportCmd .Hidden = true
78+ importCmd .Hidden = true
79+ setMetadataCmd .Hidden = true
80+ encryptCmd .Hidden = true
81+ decryptCmd .Hidden = true
82+ }
83+
84+ cmd .AddCommand (listCmd , getCmd , createCmd , deleteCmd , exportCmd , importCmd , setMetadataCmd , signCmd , verifyCmd , encryptCmd , decryptCmd )
5685 return cmd
5786}
5887
@@ -62,7 +91,7 @@ func NewListCmd() *cobra.Command {
6291 RunE : func (cmd * cobra.Command , args []string ) error {
6392 ctx , cancel := context .WithTimeout (cmd .Context (), KeystoreLoadTimeout )
6493 defer cancel ()
65- k , err := loadKeystore (ctx , cmd )
94+ k , err := loadKeystoreSignerReader (ctx , cmd )
6695 if err != nil {
6796 return err
6897 }
@@ -85,7 +114,13 @@ func NewGetCmd() *cobra.Command {
85114 cmd := cobra.Command {
86115 Use : "get" , Short : "Get keys" ,
87116 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 ) {
117+ return runKeystoreCommand [interface {
118+ ks.Reader
119+ ks.Signer
120+ }, ks.GetKeysRequest , ks.GetKeysResponse ](cmd , args , loadKeystoreSignerReader , func (ctx context.Context , k interface {
121+ ks.Reader
122+ ks.Signer
123+ }, req ks.GetKeysRequest ) (ks.GetKeysResponse , error ) {
89124 return k .GetKeys (ctx , req )
90125 })
91126 },
@@ -99,7 +134,7 @@ func NewCreateCmd() *cobra.Command {
99134 cmd := cobra.Command {
100135 Use : "create" , Short : "Create a key" ,
101136 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 ) {
137+ return runKeystoreCommand [ks.Keystore , ks. CreateKeysRequest , ks.CreateKeysResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.CreateKeysRequest ) (ks.CreateKeysResponse , error ) {
103138 return k .CreateKeys (ctx , req )
104139 })
105140 },
@@ -128,7 +163,7 @@ func NewDeleteCmd() *cobra.Command {
128163 }
129164 if ! confirmYes {
130165 // 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 , ", " )) ))
166+ _ , err = fmt . Fprintf ( cmd .OutOrStderr (), "This will permanently delete keys: %v. Type 'yes' to confirm: " , strings .Join (req .KeyNames , ", " ))
132167 if err != nil {
133168 return err
134169 }
@@ -159,7 +194,7 @@ func NewExportCmd() *cobra.Command {
159194 cmd := cobra.Command {
160195 Use : "export" , Short : "Export a key to an encrypted JSON file" ,
161196 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 ) {
197+ return runKeystoreCommand [ks.Keystore , ks. ExportKeysRequest , ks.ExportKeysResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.ExportKeysRequest ) (ks.ExportKeysResponse , error ) {
163198 return k .ExportKeys (ctx , req )
164199 })
165200 },
@@ -173,7 +208,7 @@ func NewImportCmd() *cobra.Command {
173208 cmd := cobra.Command {
174209 Use : "import" , Short : "Import an encrypted key JSON file" ,
175210 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 ) {
211+ return runKeystoreCommand [ks.Keystore , ks. ImportKeysRequest , ks.ImportKeysResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.ImportKeysRequest ) (ks.ImportKeysResponse , error ) {
177212 return k .ImportKeys (ctx , req )
178213 })
179214 },
@@ -187,7 +222,7 @@ func NewSetMetadataCmd() *cobra.Command {
187222 cmd := cobra.Command {
188223 Use : "set-metadata" , Short : "Set metadata for keys" ,
189224 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 ) {
225+ return runKeystoreCommand [ks.Keystore , ks. SetMetadataRequest , ks.SetMetadataResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.SetMetadataRequest ) (ks.SetMetadataResponse , error ) {
191226 return k .SetMetadata (ctx , req )
192227 })
193228 },
@@ -197,8 +232,13 @@ func NewSetMetadataCmd() *cobra.Command {
197232 return & cmd
198233}
199234
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 {
235+ // runKeystoreCommandGeneric is a generic helper that runs a keystore command with a custom loader function.
236+ func runKeystoreCommand [K any , Req any , Resp any ](
237+ cmd * cobra.Command ,
238+ args []string ,
239+ loader func (ctx context.Context , cmd * cobra.Command ) (K , error ),
240+ fn func (ctx context.Context , k K , req Req ) (Resp , error ),
241+ ) error {
202242 jsonBytes , err := readJSONInput (cmd )
203243 if err != nil {
204244 return err
@@ -210,7 +250,7 @@ func runKeystoreCommand[Req any, Resp any](cmd *cobra.Command, args []string, fn
210250 }
211251 ctx , cancel := context .WithTimeout (cmd .Context (), KeystoreLoadTimeout )
212252 defer cancel ()
213- k , err := loadKeystore (ctx , cmd )
253+ k , err := loader (ctx , cmd )
214254 if err != nil {
215255 return err
216256 }
@@ -233,7 +273,13 @@ func NewSignCmd() *cobra.Command {
233273 cmd := cobra.Command {
234274 Use : "sign" , Short : "Sign data with a key" ,
235275 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 ) {
276+ return runKeystoreCommand [interface {
277+ ks.Reader
278+ ks.Signer
279+ }, ks.SignRequest , ks.SignResponse ](cmd , args , loadKeystoreSignerReader , func (ctx context.Context , k interface {
280+ ks.Reader
281+ ks.Signer
282+ }, req ks.SignRequest ) (ks.SignResponse , error ) {
237283 return k .Sign (ctx , req )
238284 })
239285 },
@@ -272,7 +318,13 @@ func NewVerifyCmd() *cobra.Command {
272318 cmd := cobra.Command {
273319 Use : "verify" , Short : "Verify a signature" ,
274320 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 ) {
321+ return runKeystoreCommand [interface {
322+ ks.Reader
323+ ks.Signer
324+ }, ks.VerifyRequest , ks.VerifyResponse ](cmd , args , loadKeystoreSignerReader , func (ctx context.Context , k interface {
325+ ks.Reader
326+ ks.Signer
327+ }, req ks.VerifyRequest ) (ks.VerifyResponse , error ) {
276328 return k .Verify (ctx , req )
277329 })
278330 },
@@ -286,7 +338,7 @@ func NewEncryptCmd() *cobra.Command {
286338 cmd := cobra.Command {
287339 Use : "encrypt" , Short : "Encrypt data to a remote public key" ,
288340 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 ) {
341+ return runKeystoreCommand [ks.Keystore , ks. EncryptRequest , ks.EncryptResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.EncryptRequest ) (ks.EncryptResponse , error ) {
290342 return k .Encrypt (ctx , req )
291343 })
292344 },
@@ -325,7 +377,7 @@ func NewDecryptCmd() *cobra.Command {
325377 cmd := cobra.Command {
326378 Use : "decrypt" , Short : "Decrypt data with a key" ,
327379 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 ) {
380+ return runKeystoreCommand [ks.Keystore , ks. DecryptRequest , ks.DecryptResponse ](cmd , args , loadKeystore , func (ctx context.Context , k ks.Keystore , req ks.DecryptRequest ) (ks.DecryptResponse , error ) {
329381 return k .Decrypt (ctx , req )
330382 })
331383 },
@@ -335,38 +387,27 @@ func NewDecryptCmd() *cobra.Command {
335387 return & cmd
336388}
337389
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
390+ func loadKeystoreSignerReader (ctx context.Context , cmd * cobra.Command ) (interface {
391+ ks.Reader
392+ ks.Signer
393+ }, error ) {
394+ // Check if KMS mode is enabled
395+ kmsProfile := os .Getenv ("KEYSTORE_KMS_PROFILE" )
396+ if kmsProfile != "" {
397+ client , err := kms .NewClient (kmsProfile )
398+ if err != nil {
399+ return nil , fmt .Errorf ("create KMS client: %w" , err )
368400 }
401+ return kms .NewKeystore (client )
369402 }
403+ return loadKeystore (ctx , cmd )
404+ }
405+
406+ func loadKeystore (ctx context.Context , cmd * cobra.Command ) (ks.Keystore , error ) {
407+ // Read from environment variables only
408+ filePath := os .Getenv ("KEYSTORE_FILE_PATH" )
409+ dbURL := os .Getenv ("KEYSTORE_DB_URL" )
410+ password := os .Getenv ("KEYSTORE_PASSWORD" )
370411 if password == "" {
371412 return nil , errors .New ("keystore password is required" )
372413 }
0 commit comments