From efc00ee66671974689e94883e3a48ddad5b24fa7 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Wed, 4 Feb 2026 16:46:18 +0100 Subject: [PATCH] add evm and solana keys to corekeys --- keystore/corekeys/evm.go | 85 ++++++++++++++++++++++++++++++++ keystore/corekeys/evm_test.go | 81 ++++++++++++++++++++++++++++++ keystore/corekeys/solana.go | 85 ++++++++++++++++++++++++++++++++ keystore/corekeys/solana_test.go | 78 +++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 keystore/corekeys/evm.go create mode 100644 keystore/corekeys/evm_test.go create mode 100644 keystore/corekeys/solana.go create mode 100644 keystore/corekeys/solana_test.go diff --git a/keystore/corekeys/evm.go b/keystore/corekeys/evm.go new file mode 100644 index 0000000000..e3ab144316 --- /dev/null +++ b/keystore/corekeys/evm.go @@ -0,0 +1,85 @@ +// `corekeys` provides utilities to generate keys that are compatible with the core node +// and can be imported by it. +package corekeys + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/smartcontractkit/chainlink-common/keystore" +) + +const ( + TypeEVM = "evm" +) + +func (ks *Store) GenerateEncryptedEVMKey(ctx context.Context, password string) ([]byte, error) { + path := keystore.NewKeyPath(TypeEVM, nameDefault) + _, err := ks.CreateKeys(ctx, keystore.CreateKeysRequest{ + Keys: []keystore.CreateKeyRequest{ + { + KeyName: path.String(), + KeyType: keystore.ECDSA_S256, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to generate exportable key: %w", err) + } + + er, err := ks.ExportKeys(ctx, keystore.ExportKeysRequest{ + Keys: []keystore.ExportKeyParam{ + { + KeyName: path.String(), + Enc: keystore.EncryptionParams{ + Password: password, + ScryptParams: keystore.DefaultScryptParams, + }, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to export key: %w", err) + } + + envelope := Envelope{ + Type: TypeEVM, + Keys: er.Keys, + ExportFormat: exportFormat, + } + + data, err := json.Marshal(&envelope) + if err != nil { + return nil, fmt.Errorf("failed to marshal envelope: %w", err) + } + + return data, nil +} + +func FromEncryptedEVMKey(data []byte, password string) ([]byte, error) { + envelope := Envelope{} + err := json.Unmarshal(data, &envelope) + if err != nil { + return nil, fmt.Errorf("could not unmarshal import data into envelope: %w", err) + } + + if envelope.ExportFormat != exportFormat { + return nil, fmt.Errorf("invalid export format: %w", ErrInvalidExportFormat) + } + + if envelope.Type != TypeEVM { + return nil, fmt.Errorf("invalid key type: expected %s, got %s", TypeEVM, envelope.Type) + } + + if len(envelope.Keys) != 1 { + return nil, fmt.Errorf("expected exactly one key in envelope, got %d", len(envelope.Keys)) + } + + keypb, err := decryptKey(envelope.Keys[0].Data, password) + if err != nil { + return nil, err + } + + return keypb.PrivateKey, nil +} diff --git a/keystore/corekeys/evm_test.go b/keystore/corekeys/evm_test.go new file mode 100644 index 0000000000..16cd553aaf --- /dev/null +++ b/keystore/corekeys/evm_test.go @@ -0,0 +1,81 @@ +package corekeys + +import ( + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/keystore" +) + +func TestEVMKeyRoundTrip(t *testing.T) { + t.Parallel() + ctx := t.Context() + password := "test-password" + + st := keystore.NewMemoryStorage() + ks, err := keystore.LoadKeystore(ctx, st, "test", + keystore.WithScryptParams(keystore.FastScryptParams), + ) + require.NoError(t, err) + + coreshimKs := NewStore(ks) + + encryptedKey, err := coreshimKs.GenerateEncryptedEVMKey(ctx, password) + require.NoError(t, err) + require.NotEmpty(t, encryptedKey) + + evmKeyPath := keystore.NewKeyPath(TypeEVM, nameDefault) + getKeysResp, err := ks.GetKeys(ctx, keystore.GetKeysRequest{ + KeyNames: []string{evmKeyPath.String()}, + }) + require.NoError(t, err) + require.Len(t, getKeysResp.Keys, 1) + + storedPublicKey := getKeysResp.Keys[0].KeyInfo.PublicKey + require.NotEmpty(t, storedPublicKey) + + privateKey, err := FromEncryptedEVMKey(encryptedKey, password) + require.NoError(t, err) + require.NotEmpty(t, privateKey) + + require.Len(t, privateKey, 32) + + ecdsaPK, err := crypto.ToECDSA(privateKey) + require.NoError(t, err) + + derivedPublicKey := crypto.FromECDSAPub(&ecdsaPK.PublicKey) + require.Equal(t, storedPublicKey, derivedPublicKey) +} + +func TestEVMKeyImportWithWrongPassword(t *testing.T) { + t.Parallel() + ctx := t.Context() + password := "test-password" + wrongPassword := "wrong-password" + + st := keystore.NewMemoryStorage() + ks, err := keystore.LoadKeystore(ctx, st, "test", + keystore.WithScryptParams(keystore.FastScryptParams), + ) + require.NoError(t, err) + + coreshimKs := NewStore(ks) + + encryptedKey, err := coreshimKs.GenerateEncryptedEVMKey(ctx, password) + require.NoError(t, err) + require.NotNil(t, encryptedKey) + + _, err = FromEncryptedEVMKey(encryptedKey, wrongPassword) + require.Error(t, err) + require.Contains(t, err.Error(), "could not decrypt data") +} + +func TestEVMKeyImportInvalidFormat(t *testing.T) { + t.Parallel() + + _, err := FromEncryptedEVMKey([]byte("invalid json"), "password") + require.Error(t, err) + require.Contains(t, err.Error(), "could not unmarshal import data") +} diff --git a/keystore/corekeys/solana.go b/keystore/corekeys/solana.go new file mode 100644 index 0000000000..a4ace424c8 --- /dev/null +++ b/keystore/corekeys/solana.go @@ -0,0 +1,85 @@ +// `corekeys` provides utilities to generate keys that are compatible with the core node +// and can be imported by it. +package corekeys + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/smartcontractkit/chainlink-common/keystore" +) + +const ( + TypeSolana = "solana" +) + +func (ks *Store) GenerateEncryptedSolanaKey(ctx context.Context, password string) ([]byte, error) { + path := keystore.NewKeyPath(TypeSolana, nameDefault) + _, err := ks.CreateKeys(ctx, keystore.CreateKeysRequest{ + Keys: []keystore.CreateKeyRequest{ + { + KeyName: path.String(), + KeyType: keystore.Ed25519, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to generate exportable key: %w", err) + } + + er, err := ks.ExportKeys(ctx, keystore.ExportKeysRequest{ + Keys: []keystore.ExportKeyParam{ + { + KeyName: path.String(), + Enc: keystore.EncryptionParams{ + Password: password, + ScryptParams: keystore.DefaultScryptParams, + }, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to export key: %w", err) + } + + envelope := Envelope{ + Type: TypeSolana, + Keys: er.Keys, + ExportFormat: exportFormat, + } + + data, err := json.Marshal(&envelope) + if err != nil { + return nil, fmt.Errorf("failed to marshal envelope: %w", err) + } + + return data, nil +} + +func FromEncryptedSolanaKey(data []byte, password string) ([]byte, error) { + envelope := Envelope{} + err := json.Unmarshal(data, &envelope) + if err != nil { + return nil, fmt.Errorf("could not unmarshal import data into envelope: %w", err) + } + + if envelope.ExportFormat != exportFormat { + return nil, fmt.Errorf("invalid export format: %w", ErrInvalidExportFormat) + } + + if envelope.Type != TypeSolana { + return nil, fmt.Errorf("invalid key type: expected %s, got %s", TypeSolana, envelope.Type) + } + + if len(envelope.Keys) != 1 { + return nil, fmt.Errorf("expected exactly one key in envelope, got %d", len(envelope.Keys)) + } + + keypb, err := decryptKey(envelope.Keys[0].Data, password) + if err != nil { + return nil, err + } + + return keypb.PrivateKey, nil +} diff --git a/keystore/corekeys/solana_test.go b/keystore/corekeys/solana_test.go new file mode 100644 index 0000000000..86ddeae1cc --- /dev/null +++ b/keystore/corekeys/solana_test.go @@ -0,0 +1,78 @@ +package corekeys + +import ( + "crypto/ed25519" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/keystore" +) + +func TestSolanaKeyRoundTrip(t *testing.T) { + t.Parallel() + ctx := t.Context() + password := "test-password" + + st := keystore.NewMemoryStorage() + ks, err := keystore.LoadKeystore(ctx, st, "test", + keystore.WithScryptParams(keystore.FastScryptParams), + ) + require.NoError(t, err) + + coreshimKs := NewStore(ks) + + encryptedKey, err := coreshimKs.GenerateEncryptedSolanaKey(ctx, password) + require.NoError(t, err) + require.NotEmpty(t, encryptedKey) + + solanaKeyPath := keystore.NewKeyPath(TypeSolana, nameDefault) + getKeysResp, err := ks.GetKeys(ctx, keystore.GetKeysRequest{ + KeyNames: []string{solanaKeyPath.String()}, + }) + require.NoError(t, err) + require.Len(t, getKeysResp.Keys, 1) + + storedPublicKey := getKeysResp.Keys[0].KeyInfo.PublicKey + require.NotEmpty(t, storedPublicKey) + + privateKey, err := FromEncryptedSolanaKey(encryptedKey, password) + require.NoError(t, err) + require.NotEmpty(t, privateKey) + + require.Len(t, privateKey, 64) + + derivedPublicKey := ed25519.PrivateKey(privateKey).Public().(ed25519.PublicKey) + require.Equal(t, storedPublicKey, []byte(derivedPublicKey)) +} + +func TestSolanaKeyImportWithWrongPassword(t *testing.T) { + t.Parallel() + ctx := t.Context() + password := "test-password" + wrongPassword := "wrong-password" + + st := keystore.NewMemoryStorage() + ks, err := keystore.LoadKeystore(ctx, st, "test", + keystore.WithScryptParams(keystore.FastScryptParams), + ) + require.NoError(t, err) + + coreshimKs := NewStore(ks) + + encryptedKey, err := coreshimKs.GenerateEncryptedSolanaKey(ctx, password) + require.NoError(t, err) + require.NotNil(t, encryptedKey) + + _, err = FromEncryptedSolanaKey(encryptedKey, wrongPassword) + require.Error(t, err) + require.Contains(t, err.Error(), "could not decrypt data") +} + +func TestSolanaKeyImportInvalidFormat(t *testing.T) { + t.Parallel() + + _, err := FromEncryptedSolanaKey([]byte("invalid json"), "password") + require.Error(t, err) + require.Contains(t, err.Error(), "could not unmarshal import data") +}