Skip to content

Commit 0b4b60b

Browse files
Merge branch 'main' into PLEX-1460-delivery-acks
2 parents 6c89926 + 32197b5 commit 0b4b60b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1867
-5360
lines changed

go.mod

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ require (
77
github.com/XSAM/otelsql v0.37.0
88
github.com/andybalholm/brotli v1.1.1
99
github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c
10-
github.com/buraksezer/consistent v0.10.0
1110
github.com/bytecodealliance/wasmtime-go/v28 v28.0.0
12-
github.com/cespare/xxhash/v2 v2.3.0
1311
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
1412
github.com/dominikbraun/graph v0.23.0
1513
github.com/fxamacker/cbor/v2 v2.7.0
@@ -90,6 +88,7 @@ require (
9088
github.com/buger/goterm v1.0.4 // indirect
9189
github.com/buger/jsonparser v1.1.1 // indirect
9290
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
91+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
9392
github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect
9493
github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect
9594
github.com/fatih/color v1.18.0 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
2828
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
2929
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
3030
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
31-
github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU=
32-
github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw=
3331
github.com/bytecodealliance/wasmtime-go/v28 v28.0.0 h1:aBU8cexP2rPZ0Qz488kvn2NXvWZHL2aG1/+n7Iv+xGc=
3432
github.com/bytecodealliance/wasmtime-go/v28 v28.0.0/go.mod h1:4OCU0xAW9ycwtX4nMF4zxwgJBJ5/0eMfJiHB0wAmkV4=
3533
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=

keystore/corekeys/csa.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// `corekeys` provides utilities to generate keys that are compatible with the core node
2+
// and can be imported by it.
3+
package corekeys
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
11+
gethkeystore "github.com/ethereum/go-ethereum/accounts/keystore"
12+
"google.golang.org/protobuf/proto"
13+
14+
"github.com/smartcontractkit/chainlink-common/keystore"
15+
"github.com/smartcontractkit/chainlink-common/keystore/serialization"
16+
)
17+
18+
var (
19+
ErrInvalidExportFormat = errors.New("invalid export format")
20+
)
21+
22+
const (
23+
TypeCSA = "csa"
24+
nameDefault = "default"
25+
exportFormat = "github.com/smartcontractkit/chainlink-common/keystore/corekeys"
26+
)
27+
28+
type Store struct {
29+
keystore.Keystore
30+
}
31+
32+
type Envelope struct {
33+
Type string
34+
Keys []keystore.ExportKeyResponse
35+
ExportFormat string
36+
}
37+
38+
func NewStore(ks keystore.Keystore) *Store {
39+
return &Store{
40+
Keystore: ks,
41+
}
42+
}
43+
44+
// decryptKey decrypts an encrypted key using the provided password and returns the deserialized key.
45+
func decryptKey(encryptedData []byte, password string) (*serialization.Key, error) {
46+
encData := gethkeystore.CryptoJSON{}
47+
err := json.Unmarshal(encryptedData, &encData)
48+
if err != nil {
49+
return nil, fmt.Errorf("could not unmarshal key material into CryptoJSON: %w", err)
50+
}
51+
52+
decData, err := gethkeystore.DecryptDataV3(encData, password)
53+
if err != nil {
54+
return nil, fmt.Errorf("could not decrypt data: %w", err)
55+
}
56+
57+
keypb := &serialization.Key{}
58+
err = proto.Unmarshal(decData, keypb)
59+
if err != nil {
60+
return nil, fmt.Errorf("could not unmarshal key into serialization.Key: %w", err)
61+
}
62+
63+
return keypb, nil
64+
}
65+
66+
func (ks *Store) GenerateEncryptedCSAKey(ctx context.Context, password string) ([]byte, error) {
67+
path := keystore.NewKeyPath(TypeCSA, nameDefault)
68+
_, err := ks.CreateKeys(ctx, keystore.CreateKeysRequest{
69+
Keys: []keystore.CreateKeyRequest{
70+
{
71+
KeyName: path.String(),
72+
KeyType: keystore.Ed25519,
73+
},
74+
},
75+
})
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to generate exportable key: %w", err)
78+
}
79+
80+
er, err := ks.ExportKeys(ctx, keystore.ExportKeysRequest{
81+
Keys: []keystore.ExportKeyParam{
82+
{
83+
KeyName: path.String(),
84+
Enc: keystore.EncryptionParams{
85+
Password: password,
86+
ScryptParams: keystore.DefaultScryptParams,
87+
},
88+
},
89+
},
90+
})
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to export key: %w", err)
93+
}
94+
95+
envelope := Envelope{
96+
Type: TypeCSA,
97+
Keys: er.Keys,
98+
ExportFormat: exportFormat,
99+
}
100+
101+
data, err := json.Marshal(&envelope)
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
104+
}
105+
106+
return data, nil
107+
}
108+
109+
func FromEncryptedCSAKey(data []byte, password string) ([]byte, error) {
110+
envelope := Envelope{}
111+
err := json.Unmarshal(data, &envelope)
112+
if err != nil {
113+
return nil, fmt.Errorf("could not unmarshal import data into envelope: %w", err)
114+
}
115+
116+
if envelope.ExportFormat != exportFormat {
117+
return nil, fmt.Errorf("invalid export format: %w", ErrInvalidExportFormat)
118+
}
119+
120+
if envelope.Type != TypeCSA {
121+
return nil, fmt.Errorf("invalid key type: expected %s, got %s", TypeCSA, envelope.Type)
122+
}
123+
124+
if len(envelope.Keys) != 1 {
125+
return nil, fmt.Errorf("expected exactly one key in envelope, got %d", len(envelope.Keys))
126+
}
127+
128+
keypb, err := decryptKey(envelope.Keys[0].Data, password)
129+
if err != nil {
130+
return nil, err
131+
}
132+
133+
return keypb.PrivateKey, nil
134+
}

keystore/corekeys/csa_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package corekeys
2+
3+
import (
4+
"crypto/ed25519"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/smartcontractkit/chainlink-common/keystore"
10+
)
11+
12+
func TestCSAKeyRoundTrip(t *testing.T) {
13+
t.Parallel()
14+
ctx := t.Context()
15+
password := "test-password"
16+
17+
st := keystore.NewMemoryStorage()
18+
ks, err := keystore.LoadKeystore(ctx, st, "test",
19+
keystore.WithScryptParams(keystore.FastScryptParams),
20+
)
21+
require.NoError(t, err)
22+
23+
coreshimKs := NewStore(ks)
24+
25+
encryptedKey, err := coreshimKs.GenerateEncryptedCSAKey(ctx, password)
26+
require.NoError(t, err)
27+
require.NotEmpty(t, encryptedKey)
28+
29+
csaKeyPath := keystore.NewKeyPath(TypeCSA, nameDefault)
30+
getKeysResp, err := ks.GetKeys(ctx, keystore.GetKeysRequest{
31+
KeyNames: []string{csaKeyPath.String()},
32+
})
33+
require.NoError(t, err)
34+
require.Len(t, getKeysResp.Keys, 1)
35+
36+
storedPublicKey := getKeysResp.Keys[0].KeyInfo.PublicKey
37+
require.NotEmpty(t, storedPublicKey)
38+
39+
privateKey, err := FromEncryptedCSAKey(encryptedKey, password)
40+
require.NoError(t, err)
41+
require.NotEmpty(t, privateKey)
42+
43+
require.Len(t, privateKey, 64)
44+
45+
derivedPublicKey := ed25519.PrivateKey(privateKey).Public().(ed25519.PublicKey)
46+
require.Equal(t, storedPublicKey, []byte(derivedPublicKey))
47+
}
48+
49+
func TestCSAKeyImportWithWrongPassword(t *testing.T) {
50+
t.Parallel()
51+
ctx := t.Context()
52+
password := "test-password"
53+
wrongPassword := "wrong-password"
54+
55+
st := keystore.NewMemoryStorage()
56+
ks, err := keystore.LoadKeystore(ctx, st, "test",
57+
keystore.WithScryptParams(keystore.FastScryptParams),
58+
)
59+
require.NoError(t, err)
60+
61+
coreshimKs := NewStore(ks)
62+
63+
encryptedKey, err := coreshimKs.GenerateEncryptedCSAKey(ctx, password)
64+
require.NoError(t, err)
65+
require.NotNil(t, encryptedKey)
66+
67+
_, err = FromEncryptedCSAKey(encryptedKey, wrongPassword)
68+
require.Error(t, err)
69+
require.Contains(t, err.Error(), "could not decrypt data")
70+
}
71+
72+
func TestCSAKeyImportInvalidFormat(t *testing.T) {
73+
t.Parallel()
74+
75+
_, err := FromEncryptedCSAKey([]byte("invalid json"), "password")
76+
require.Error(t, err)
77+
require.Contains(t, err.Error(), "could not unmarshal import data")
78+
}

keystore/corekeys/ocrkeybundle.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package corekeys
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
chainselectors "github.com/smartcontractkit/chain-selectors"
10+
"github.com/smartcontractkit/chainlink-common/keystore"
11+
"github.com/smartcontractkit/chainlink-common/keystore/ocr2offchain"
12+
)
13+
14+
const (
15+
TypeOCR = "ocr"
16+
PrefixOCR2Onchain = "ocr2_onchain"
17+
)
18+
19+
type OCRKeyBundle struct {
20+
ChainType string
21+
OffchainSigningKey []byte
22+
OffchainEncryptionKey []byte
23+
OnchainSigningKey []byte
24+
}
25+
26+
func (ks *Store) GenerateEncryptedOCRKeyBundle(ctx context.Context, chainType string, password string) ([]byte, error) {
27+
_, err := ocr2offchain.CreateOCR2OffchainKeyring(ctx, ks.Keystore, nameDefault)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
var onchainKeyPath keystore.KeyPath
33+
switch chainType {
34+
case chainselectors.FamilyEVM:
35+
path := keystore.NewKeyPath(PrefixOCR2Onchain, nameDefault, chainType)
36+
_, ierr := ks.CreateKeys(ctx, keystore.CreateKeysRequest{
37+
Keys: []keystore.CreateKeyRequest{
38+
{
39+
KeyName: path.String(),
40+
KeyType: keystore.ECDSA_S256,
41+
},
42+
},
43+
})
44+
if ierr != nil {
45+
return nil, fmt.Errorf("failed to generate exportable key: %w", ierr)
46+
}
47+
48+
onchainKeyPath = path
49+
default:
50+
return nil, fmt.Errorf("unsupported chain type: %s", chainType)
51+
}
52+
53+
er, err := ks.ExportKeys(ctx, keystore.ExportKeysRequest{
54+
Keys: []keystore.ExportKeyParam{
55+
{
56+
KeyName: keystore.NewKeyPath(ocr2offchain.PrefixOCR2Offchain, nameDefault, ocr2offchain.OCR2OffchainSigning).String(),
57+
Enc: keystore.EncryptionParams{
58+
Password: password,
59+
ScryptParams: keystore.DefaultScryptParams,
60+
},
61+
},
62+
{
63+
KeyName: keystore.NewKeyPath(ocr2offchain.PrefixOCR2Offchain, nameDefault, ocr2offchain.OCR2OffchainEncryption).String(),
64+
Enc: keystore.EncryptionParams{
65+
Password: password,
66+
ScryptParams: keystore.DefaultScryptParams,
67+
},
68+
},
69+
{
70+
KeyName: onchainKeyPath.String(),
71+
Enc: keystore.EncryptionParams{
72+
Password: password,
73+
ScryptParams: keystore.DefaultScryptParams,
74+
},
75+
},
76+
},
77+
})
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to export OCR key bundle: %w", err)
80+
}
81+
82+
envelope := Envelope{
83+
Type: TypeOCR,
84+
Keys: er.Keys,
85+
ExportFormat: exportFormat,
86+
}
87+
88+
data, err := json.Marshal(&envelope)
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to marshal OCR key bundle envelope: %w", err)
91+
}
92+
93+
return data, nil
94+
}
95+
96+
func FromEncryptedOCRKeyBundle(data []byte, password string) (*OCRKeyBundle, error) {
97+
envelope := Envelope{}
98+
err := json.Unmarshal(data, &envelope)
99+
if err != nil {
100+
return nil, fmt.Errorf("could not unmarshal import data into envelope: %w", err)
101+
}
102+
103+
if envelope.ExportFormat != exportFormat {
104+
return nil, fmt.Errorf("invalid export format: %w", ErrInvalidExportFormat)
105+
}
106+
107+
if envelope.Type != TypeOCR {
108+
return nil, fmt.Errorf("invalid key type: expected %s, got %s", TypeOCR, envelope.Type)
109+
}
110+
111+
if len(envelope.Keys) != 3 {
112+
return nil, fmt.Errorf("expected exactly three keys in envelope, got %d", len(envelope.Keys))
113+
}
114+
115+
bundle := &OCRKeyBundle{}
116+
117+
for _, key := range envelope.Keys {
118+
keypb, err := decryptKey(key.Data, password)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
if strings.Contains(key.KeyName, ocr2offchain.OCR2OffchainSigning) {
124+
bundle.OffchainSigningKey = keypb.PrivateKey
125+
} else if strings.Contains(key.KeyName, ocr2offchain.OCR2OffchainEncryption) {
126+
bundle.OffchainEncryptionKey = keypb.PrivateKey
127+
} else if strings.Contains(key.KeyName, PrefixOCR2Onchain) {
128+
bundle.OnchainSigningKey = keypb.PrivateKey
129+
// Extract chain type from the key path
130+
keyPath := keystore.NewKeyPathFromString(key.KeyName)
131+
bundle.ChainType = strings.ToLower(keyPath.Base())
132+
}
133+
}
134+
135+
return bundle, nil
136+
}

0 commit comments

Comments
 (0)