Skip to content

Commit 6578122

Browse files
committed
Merge branch 'main' into INFOPLAT-3250/test-lint
2 parents 2f6e1a5 + ee57ba2 commit 6578122

14 files changed

Lines changed: 986 additions & 86 deletions

File tree

.github/workflows/golangci_lint.yml

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ jobs:
2222
if: ${{ github.event_name != 'merge_group' }}
2323
needs: detect-modules
2424
runs-on: ubuntu-latest
25-
env:
26-
GO_DIR: ${{ matrix.module }}
2725
# Modules are independent
2826
continue-on-error: true
2927
strategy:
@@ -66,31 +64,6 @@ jobs:
6664
golangci-lint-args: "--output.text.path=stdout --output.checkstyle.path=${{ github.workspace }}/${{ matrix.module }}/golangci-lint-report.xml"
6765
only-new-issues: false
6866

69-
- name: Debug
70-
if: ${{ always() }}
71-
run: |
72-
module_dir="${{ matrix.module }}"
73-
report_path="${module_dir%/}/golangci-lint-report.xml"
74-
echo "Debugging lint results for module ${module_dir}"
75-
echo "GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
76-
echo "Current working directory: $(pwd)"
77-
echo ""
78-
echo "=== Searching for all XML files from workspace root ==="
79-
find "$GITHUB_WORKSPACE" -name "*.xml" -type f 2>/dev/null || echo "No XML files found"
80-
echo ""
81-
echo "=== Checking expected location ==="
82-
if [ -f "${report_path}" ]; then
83-
echo "✓ File EXISTS at ${report_path}"
84-
ls -lh "${report_path}"
85-
echo "Contents:"
86-
cat "${report_path}"
87-
else
88-
echo "✗ File DOES NOT EXIST at ${report_path}"
89-
fi
90-
echo ""
91-
echo "=== Listing all files in module directory ==="
92-
ls -la "${module_dir%/}/" || echo "Directory doesn't exist"
93-
9467
golangci-lint:
9568
# Required sink job that waits for all lint jobs to complete
9669
if: ${{ github.event_name != 'merge_group' }}

keystore/cli/cli.go

Lines changed: 112 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
3640
KEYSTORE_DB_URL: is the postgres connection URL. Only use this if your keystore is stored in a pg database.
3741
Requires a pg database with a 'encrypted_keystore' table with the following schema:
3842
CREATE 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
}

keystore/cli/cli_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ func setupKeystore(t *testing.T) func(t *testing.T) {
2222
require.NoError(t, err)
2323
t.Setenv("KEYSTORE_FILE_PATH", keystoreFile)
2424
t.Setenv("KEYSTORE_PASSWORD", "testpassword")
25+
// Set to empty string to test regular keystore mode.
26+
t.Setenv("KEYSTORE_KMS_PROFILE", "")
2527
return func(t *testing.T) {
2628
f.Close()
2729
os.RemoveAll(tempDir)
@@ -107,6 +109,32 @@ func TestAdminCLI(t *testing.T) {
107109
require.Equal(t, "testkey", resp.Keys[0].KeyInfo.Name)
108110
// Metadata is []byte, Go's JSON unmarshaler automatically decodes base64 strings
109111
require.Equal(t, "my-custom-metadata", string(resp.Keys[0].KeyInfo.Metadata))
112+
originalPublicKey := resp.Keys[0].KeyInfo.PublicKey
113+
114+
// Rename testkey to renamedkey.
115+
_, err = runCommand(t, nil, "rename", "-d", `{"OldName": "testkey", "NewName": "renamedkey"}`)
116+
require.NoError(t, err)
117+
118+
// Verify the old name doesn't exist.
119+
out, err = runCommand(t, nil, "get", "-d", `{"KeyNames": ["testkey"]}`)
120+
require.Error(t, err)
121+
122+
// Verify the new name exists with the same key material.
123+
out, err = runCommand(t, nil, "get", "-d", `{"KeyNames": ["renamedkey"]}`)
124+
require.NoError(t, err)
125+
resp = ks.GetKeysResponse{}
126+
err = json.Unmarshal(out.Bytes(), &resp)
127+
require.NoError(t, err)
128+
require.Len(t, resp.Keys, 1)
129+
require.Equal(t, "renamedkey", resp.Keys[0].KeyInfo.Name)
130+
require.Equal(t, ks.X25519, resp.Keys[0].KeyInfo.KeyType)
131+
require.Equal(t, originalPublicKey, resp.Keys[0].KeyInfo.PublicKey)
132+
// Metadata should be preserved
133+
require.Equal(t, "my-custom-metadata", string(resp.Keys[0].KeyInfo.Metadata))
134+
135+
// Rename it back to testkey for cleanup.
136+
_, err = runCommand(t, nil, "rename", "-d", `{"OldName": "renamedkey", "NewName": "testkey"}`)
137+
require.NoError(t, err)
110138

111139
// Delete the keys with confirmation.
112140
out, err = runCommand(t, bytes.NewBufferString("yes\n"), "delete", "-d", `{"KeyNames": ["testkey", "testkey2"]}`)

keystore/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/smartcontractkit/chainlink-common/keystore
33
go 1.25.3
44

55
require (
6+
github.com/aws/aws-sdk-go v1.55.5
67
github.com/ethereum/go-ethereum v1.16.2
78
github.com/jmoiron/sqlx v1.4.0
89
github.com/lib/pq v1.10.9
@@ -58,6 +59,7 @@ require (
5859
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
5960
github.com/jackc/pgtype v1.14.4 // indirect
6061
github.com/jackc/pgx/v4 v4.18.3 // indirect
62+
github.com/jmespath/go-jmespath v0.4.0 // indirect
6163
github.com/json-iterator/go v1.1.12 // indirect
6264
github.com/klauspost/compress v1.18.0 // indirect
6365
github.com/klauspost/cpuid/v2 v2.2.10 // indirect

0 commit comments

Comments
 (0)