Skip to content

Commit 75f426c

Browse files
authored
Merge branch 'main' into PRIV-192
2 parents c4fc250 + 415d51d commit 75f426c

Some content is hidden

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

44 files changed

+2670
-103
lines changed

go.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ flowchart LR
99
chainlink-common --> chainlink-common/pkg/values
1010
chainlink-common --> chainlink-protos/billing/go
1111
chainlink-common --> chainlink-protos/cre/go
12+
chainlink-common --> chainlink-protos/linking-service/go
1213
chainlink-common --> chainlink-protos/storage-service
1314
chainlink-common --> freeport
1415
chainlink-common --> grpc-proxy
@@ -24,6 +25,8 @@ flowchart LR
2425
click chainlink-protos/billing/go href "https://github.com/smartcontractkit/chainlink-protos"
2526
chainlink-protos/cre/go
2627
click chainlink-protos/cre/go href "https://github.com/smartcontractkit/chainlink-protos"
28+
chainlink-protos/linking-service/go
29+
click chainlink-protos/linking-service/go href "https://github.com/smartcontractkit/chainlink-protos"
2730
chainlink-protos/storage-service
2831
click chainlink-protos/storage-service href "https://github.com/smartcontractkit/chainlink-protos"
2932
chainlink-protos/workflows/go
@@ -46,6 +49,7 @@ flowchart LR
4649
subgraph chainlink-protos-repo[chainlink-protos]
4750
chainlink-protos/billing/go
4851
chainlink-protos/cre/go
52+
chainlink-protos/linking-service/go
4953
chainlink-protos/storage-service
5054
chainlink-protos/workflows/go
5155
end

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ require (
4040
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4
4141
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976
4242
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2
43+
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b
4344
github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0
45+
github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20250822025801-598d3d86f873
4446
github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e
4547
github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7
4648
github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358
@@ -137,7 +139,6 @@ require (
137139
github.com/rogpeppe/go-internal v1.13.1 // indirect
138140
github.com/ryanuber/go-glob v1.0.0 // indirect
139141
github.com/sanity-io/litter v1.5.5 // indirect
140-
github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20250822025801-598d3d86f873 // indirect
141142
github.com/stretchr/objx v0.5.2 // indirect
142143
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
143144
github.com/x448/float16 v0.8.4 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-87
332332
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20250722225531-876fd6b94976/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA=
333333
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2 h1:1/KdO5AbUr3CmpLjMPuJXPo2wHMbfB8mldKLsg7D4M8=
334334
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q=
335+
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM=
336+
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY=
335337
github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 h1:B7itmjy+CMJ26elVw/cAJqqhBQ3Xa/mBYWK0/rQ5MuI=
336338
github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0/go.mod h1:h6kqaGajbNRrezm56zhx03p0mVmmA2xxj7E/M4ytLUA=
337339
github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20250822025801-598d3d86f873 h1:8/qwOmcdSFa8A6ecnj3eH/mwNx7Ybw2tjQFydDymtOc=

keystore/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Keystore
2+
Design principles:
3+
- Use structs for typed extensibility of the interfaces. Easy
4+
to wrap via a network layer if needed.
5+
- Storage abstract. Keystore interfaces can be implemented with memory, file, database, etc. for storage to be useable in a variety of
6+
contexts. Use write through caching to maintain synchronization between in memory keys and stored keys.
7+
- Only the Admin interface mutates the keystore, all other interfaces are read only. Admin interface
8+
is plural/batched to support atomic batched mutations.
9+
- Client side key naming. Keystore itself doesn't impose certain key algorithims/curves be used for specific contexts, it just supports a the minimum viable set of algorithms/curves for chainlink wide use cases. Clients define a name for each key which represents
10+
the context in which they wish to use it.
11+
- Common serialization/encryption for all storage types. Protobuf serialization (compact, versioned) for key material and then key material encrypted before persistence with a passphase.
12+
13+
Notes
14+
- keystore/internal is copied from https://github.com/smartcontractkit/chainlink/blob/develop/core/services/keystore/internal/raw.go#L3. Intention is to switch to core to use this library at which point we can remove the core copy.

keystore/admin.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package keystore
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"crypto/ed25519"
7+
"crypto/rand"
8+
"fmt"
9+
"maps"
10+
"time"
11+
12+
"golang.org/x/crypto/curve25519"
13+
14+
"github.com/ethereum/go-ethereum/crypto"
15+
"github.com/smartcontractkit/chainlink-common/keystore/internal"
16+
)
17+
18+
var (
19+
ErrKeyAlreadyExists = fmt.Errorf("key already exists")
20+
ErrInvalidKeyName = fmt.Errorf("invalid key name")
21+
ErrKeyNotFound = fmt.Errorf("key not found")
22+
ErrUnsupportedKeyType = fmt.Errorf("unsupported key type")
23+
)
24+
25+
type CreateKeysRequest struct {
26+
Keys []CreateKeyRequest
27+
}
28+
29+
type CreateKeyRequest struct {
30+
KeyName string
31+
KeyType KeyType
32+
}
33+
34+
type CreateKeysResponse struct {
35+
Keys []CreateKeyResponse
36+
}
37+
38+
type CreateKeyResponse struct {
39+
KeyInfo KeyInfo
40+
}
41+
42+
type DeleteKeysRequest struct {
43+
KeyNames []string
44+
}
45+
46+
type DeleteKeysResponse struct{}
47+
48+
type ImportKeysRequest struct {
49+
Keys []ImportKeyRequest
50+
}
51+
52+
type ImportKeyRequest struct {
53+
KeyName string
54+
KeyType KeyType
55+
Data []byte
56+
}
57+
58+
type ImportKeysResponse struct{}
59+
60+
type ExportKeyParam struct {
61+
KeyName string
62+
Enc EncryptionParams
63+
}
64+
65+
type ExportKeysRequest struct {
66+
Keys []ExportKeyParam
67+
}
68+
69+
type ExportKeysResponse struct {
70+
Keys []ExportKeyResponse
71+
}
72+
73+
type ExportKeyResponse struct {
74+
KeyName string
75+
Data []byte
76+
}
77+
78+
type SetMetadataRequest struct {
79+
Updates []SetMetadataUpdate
80+
}
81+
82+
type SetMetadataUpdate struct {
83+
KeyName string
84+
Metadata []byte
85+
}
86+
87+
type SetMetadataResponse struct{}
88+
89+
type Admin interface {
90+
CreateKeys(ctx context.Context, req CreateKeysRequest) (CreateKeysResponse, error)
91+
DeleteKeys(ctx context.Context, req DeleteKeysRequest) (DeleteKeysResponse, error)
92+
ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error)
93+
ExportKeys(ctx context.Context, req ExportKeysRequest) (ExportKeysResponse, error)
94+
SetMetadata(ctx context.Context, req SetMetadataRequest) (SetMetadataResponse, error)
95+
}
96+
97+
// UnimplementedAdmin returns ErrUnimplemented for all Admin methods.
98+
type UnimplementedAdmin struct{}
99+
100+
func (UnimplementedAdmin) CreateKeys(ctx context.Context, req CreateKeysRequest) (CreateKeysResponse, error) {
101+
return CreateKeysResponse{}, fmt.Errorf("Admin.CreateKeys: %w", ErrUnimplemented)
102+
}
103+
104+
func (UnimplementedAdmin) DeleteKeys(ctx context.Context, req DeleteKeysRequest) (DeleteKeysResponse, error) {
105+
return DeleteKeysResponse{}, fmt.Errorf("Admin.DeleteKeys: %w", ErrUnimplemented)
106+
}
107+
108+
func (UnimplementedAdmin) ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error) {
109+
return ImportKeysResponse{}, fmt.Errorf("Admin.ImportKeys: %w", ErrUnimplemented)
110+
}
111+
112+
func (UnimplementedAdmin) ExportKeys(ctx context.Context, req ExportKeysRequest) (ExportKeysResponse, error) {
113+
return ExportKeysResponse{}, fmt.Errorf("Admin.ExportKeys: %w", ErrUnimplemented)
114+
}
115+
116+
func (UnimplementedAdmin) SetMetadata(ctx context.Context, req SetMetadataRequest) (SetMetadataResponse, error) {
117+
return SetMetadataResponse{}, fmt.Errorf("Admin.SetMetadata: %w", ErrUnimplemented)
118+
}
119+
120+
func ValidKeyName(name string) error {
121+
if name == "" {
122+
return fmt.Errorf("key name cannot be empty")
123+
}
124+
// Just a sanity bound.
125+
if len(name) > 1_000 {
126+
return fmt.Errorf("key name cannot be longer than 1000 characters")
127+
}
128+
return nil
129+
}
130+
131+
func (ks *keystore) CreateKeys(ctx context.Context, req CreateKeysRequest) (CreateKeysResponse, error) {
132+
ks.mu.Lock()
133+
defer ks.mu.Unlock()
134+
135+
ksCopy := maps.Clone(ks.keystore)
136+
var responses []CreateKeyResponse
137+
for _, keyReq := range req.Keys {
138+
if _, ok := ksCopy[keyReq.KeyName]; ok {
139+
return CreateKeysResponse{}, fmt.Errorf("%w: %s", ErrKeyAlreadyExists, keyReq.KeyName)
140+
}
141+
if err := ValidKeyName(keyReq.KeyName); err != nil {
142+
return CreateKeysResponse{}, fmt.Errorf("%w: %s", ErrInvalidKeyName, err)
143+
}
144+
switch keyReq.KeyType {
145+
case Ed25519:
146+
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
147+
if err != nil {
148+
return CreateKeysResponse{}, fmt.Errorf("failed to generate Ed25519 key: %w", err)
149+
}
150+
publicKey, err := publicKeyFromPrivateKey(internal.NewRaw(privateKey), keyReq.KeyType)
151+
if err != nil {
152+
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
153+
}
154+
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey), publicKey, time.Now(), []byte{})
155+
case EcdsaSecp256k1:
156+
privateKey, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
157+
if err != nil {
158+
return CreateKeysResponse{}, fmt.Errorf("failed to generate EcdsaSecp256k1 key: %w", err)
159+
}
160+
publicKey, err := publicKeyFromPrivateKey(internal.NewRaw(privateKey.D.Bytes()), keyReq.KeyType)
161+
if err != nil {
162+
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
163+
}
164+
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey.D.Bytes()), publicKey, time.Now(), []byte{})
165+
case X25519:
166+
privateKey := [curve25519.ScalarSize]byte{}
167+
_, err := rand.Read(privateKey[:])
168+
if err != nil {
169+
return CreateKeysResponse{}, fmt.Errorf("failed to generate Curve25519 key: %w", err)
170+
}
171+
publicKey, err := publicKeyFromPrivateKey(internal.NewRaw(privateKey[:]), keyReq.KeyType)
172+
if err != nil {
173+
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
174+
}
175+
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey[:]), publicKey, time.Now(), []byte{})
176+
default:
177+
return CreateKeysResponse{}, fmt.Errorf("%w: %s", ErrUnsupportedKeyType, keyReq.KeyType)
178+
}
179+
180+
created := ksCopy[keyReq.KeyName].createdAt
181+
k := ksCopy[keyReq.KeyName]
182+
responses = append(responses, CreateKeyResponse{
183+
KeyInfo: newKeyInfo(keyReq.KeyName, keyReq.KeyType, created, k.publicKey, k.metadata),
184+
})
185+
}
186+
187+
// Persist it to storage.
188+
if err := ks.save(ctx, ksCopy); err != nil {
189+
return CreateKeysResponse{}, fmt.Errorf("failed to save keystore: %w", err)
190+
}
191+
// If we succeed to save, update the in memory keystore.
192+
ks.keystore = ksCopy
193+
return CreateKeysResponse{Keys: responses}, nil
194+
}
195+
196+
func (k *keystore) DeleteKeys(ctx context.Context, req DeleteKeysRequest) (DeleteKeysResponse, error) {
197+
k.mu.Lock()
198+
defer k.mu.Unlock()
199+
200+
ksCopy := maps.Clone(k.keystore)
201+
for _, name := range req.KeyNames {
202+
if _, ok := ksCopy[name]; !ok {
203+
return DeleteKeysResponse{}, fmt.Errorf("%w: %s", ErrKeyNotFound, name)
204+
}
205+
delete(ksCopy, name)
206+
}
207+
if err := k.save(ctx, ksCopy); err != nil {
208+
return DeleteKeysResponse{}, fmt.Errorf("failed to save keystore: %w", err)
209+
}
210+
k.keystore = ksCopy
211+
return DeleteKeysResponse{}, nil
212+
}
213+
214+
func (k *keystore) ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error) {
215+
return ImportKeysResponse{}, nil
216+
}
217+
218+
func (k *keystore) ExportKeys(ctx context.Context, req ExportKeysRequest) (ExportKeysResponse, error) {
219+
return ExportKeysResponse{}, nil
220+
}
221+
222+
func (ks *keystore) SetMetadata(ctx context.Context, req SetMetadataRequest) (SetMetadataResponse, error) {
223+
return SetMetadataResponse{}, nil
224+
}

0 commit comments

Comments
 (0)