diff --git a/keystore/README.md b/keystore/README.md
index 468c829909..dd5d77a031 100644
--- a/keystore/README.md
+++ b/keystore/README.md
@@ -103,6 +103,59 @@ func main() {
}
```
+##### Google Cloud KMS
+```go
+package main
+
+import (
+ "context"
+ "crypto/sha256"
+
+ "github.com/smartcontractkit/chainlink-common/keystore"
+ gcpkms "github.com/smartcontractkit/chainlink-common/keystore/gcpkms"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Create a Cloud KMS backed keystore. Credentials come from Application Default
+ // Credentials (Workload Identity on GKE, GOOGLE_APPLICATION_CREDENTIALS locally).
+ client, _ := gcpkms.NewClient(ctx)
+ defer client.Close()
+ ks, _ := gcpkms.NewKeystore(client)
+
+ // Cloud KMS key names are CryptoKeyVersion resource names:
+ // projects/
/locations//keyRings//cryptoKeys//cryptoKeyVersions/
+ // A name always names exactly one version; rotate by configuring the new version's name.
+ versionName := "projects/my-project/locations/us-central1/keyRings/my-ring/cryptoKeys/my-key/cryptoKeyVersions/1"
+
+ // GetKeys requires explicit key names.
+ keysResp, _ := ks.GetKeys(ctx, keystore.GetKeysRequest{
+ KeyNames: []string{versionName},
+ })
+
+ data := []byte("hello world")
+ hash := sha256.Sum256(data)
+ signResp, _ := ks.Sign(ctx, keystore.SignRequest{
+ KeyName: versionName,
+ Data: hash[:],
+ })
+
+ verifyResp, _ := ks.Verify(ctx, keystore.VerifyRequest{
+ KeyType: keysResp.Keys[0].KeyInfo.KeyType,
+ PublicKey: keysResp.Keys[0].KeyInfo.PublicKey,
+ Data: hash[:],
+ Signature: signResp.Signature,
+ })
+ // verifyResp.Valid == true
+}
+```
+Note: unlike the file/DB and AWS backends, the GCP backend does **not** support listing key
+rings. `GetKeys` requires each key name to be provided explicitly (a fully-qualified
+`CryptoKeyVersion` resource name) and errors on an empty request. Callers relying
+on "list all keys when no names are provided" (e.g. `keystore.CoreKeystore.Accounts`) must be
+configured with explicit key names before wiring them to a GCP-backed keystore.
+
#### Encryption
```go
@@ -177,6 +230,9 @@ export KEYSTORE_KMS_PROFILE="my-aws-profile"
keys list # Lists KMS keys
keys sign -d '{"KeyName": "arn:aws:kms:us-west-2:123456789012:key/abc123", "Data": ""}'
```
+Note: the CLI's KMS mode is currently **AWS-only** (`KEYSTORE_KMS_PROFILE` selects an AWS profile).
+The Google Cloud KMS backend is available programmatically via `gcpkms` (see above) but has no CLI
+selector yet.
### Design Principles
- **Embeddable CLI** The cli package is designed to support
diff --git a/keystore/cli/cli.go b/keystore/cli/cli.go
index 24cc89817f..a21536b2f8 100644
--- a/keystore/cli/cli.go
+++ b/keystore/cli/cli.go
@@ -32,6 +32,8 @@ CLI for managing keystore keys.
If KEYSTORE_KMS_PROFILE is set, will load the keystore from KMS.
KEYSTORE_KMS_PROFILE: is the AWS profile to use for KMS (region will be taken from the profile).
+Note: the CLI KMS mode is currently AWS-only; the Google Cloud KMS backend (keystore/gcpkms) has
+no CLI selector yet and must be used programmatically.
Otherwise, will load the keystore from a file or database.
KEYSTORE_PASSWORD: password used to encrypt the key material before storage, must be provided.
@@ -407,7 +409,8 @@ func loadKeystoreSignerReader(ctx context.Context, cmd *cobra.Command) (interfac
ks.Reader
ks.Signer
}, error) {
- // Check if KMS mode is enabled
+ // Check if KMS mode is enabled. AWS only: KEYSTORE_KMS_PROFILE selects an AWS profile.
+ // There is no GCP (keystore/gcpkms) selector yet; use the gcpkms package programmatically.
kmsProfile := os.Getenv("KEYSTORE_KMS_PROFILE")
if kmsProfile != "" {
client, err := kms.NewClient(ctx, kms.ClientOptions{
diff --git a/keystore/gcpkms/client.go b/keystore/gcpkms/client.go
new file mode 100644
index 0000000000..a94b4c8899
--- /dev/null
+++ b/keystore/gcpkms/client.go
@@ -0,0 +1,66 @@
+package gcpkms
+
+import (
+ "context"
+ "fmt"
+
+ apiv1 "cloud.google.com/go/kms/apiv1"
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "github.com/googleapis/gax-go/v2"
+ "google.golang.org/api/option"
+)
+
+// Client is an interface that defines the operations needed by the keystore. It keeps the keystore
+// independent of the generated Google Cloud KMS client.
+//
+// Every method operates on a single CryptoKeyVersion, so each can be authorized with per-key IAM
+// bindings; a deployment binds exactly the keys it configures and nothing else.
+//
+// These methods are based on the Google Cloud KMS Go client interface.
+// https://pkg.go.dev/cloud.google.com/go/kms/apiv1
+type Client interface {
+ GetCryptoKeyVersion(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error)
+ GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error)
+ AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error)
+}
+
+// NewClient constructs a new Google Cloud KMS client using the Go SDK.
+//
+// Credentials always come from Application Default Credentials, which covers both production (GKE
+// Workload Identity, GCE/Cloud Run service accounts) and local development (`gcloud auth
+// application-default login`, or GOOGLE_APPLICATION_CREDENTIALS pointing at a service-account key file).
+//
+// opts is passed through to the SDK for the cases ADC does not cover a custom endpoint or
+// emulator, a quota project, a non-default token source.
+// https://cloud.google.com/docs/authentication/application-default-credentials
+func NewClient(ctx context.Context, opts ...option.ClientOption) (*SDKClient, error) {
+ client, err := apiv1.NewKeyManagementClient(ctx, opts...)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create Google Cloud KMS client: %w", err)
+ }
+ return &SDKClient{client: client}, nil
+}
+
+// SDKClient adapts the generated Cloud KMS client to this package's [Client] interface and owns
+// the underlying transport, so callers must Close it.
+type SDKClient struct {
+ client *apiv1.KeyManagementClient
+}
+
+var _ Client = (*SDKClient)(nil)
+
+func (c *SDKClient) GetCryptoKeyVersion(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) {
+ return c.client.GetCryptoKeyVersion(ctx, req, opts...)
+}
+
+func (c *SDKClient) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) {
+ return c.client.GetPublicKey(ctx, req, opts...)
+}
+
+func (c *SDKClient) AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) {
+ return c.client.AsymmetricSign(ctx, req, opts...)
+}
+
+func (c *SDKClient) Close() error {
+ return c.client.Close()
+}
diff --git a/keystore/gcpkms/fake_client.go b/keystore/gcpkms/fake_client.go
new file mode 100644
index 0000000000..b5a4c644e6
--- /dev/null
+++ b/keystore/gcpkms/fake_client.go
@@ -0,0 +1,288 @@
+package gcpkms
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/googleapis/gax-go/v2"
+ "google.golang.org/protobuf/types/known/timestamppb"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+
+ "github.com/smartcontractkit/chainlink-common/keystore"
+ "github.com/smartcontractkit/chainlink-common/keystore/internal"
+ "github.com/smartcontractkit/chainlink-common/keystore/kms"
+)
+
+// cryptoKeyVersionsSegment separates a CryptoKey resource name from its version number in a
+// CryptoKeyVersion resource name.
+const cryptoKeyVersionsSegment = "/cryptoKeyVersions/"
+
+// Key identifies one in-memory CryptoKeyVersion held by FakeGCPKMSClient. KeyID is a CryptoKey
+// resource name (projects//locations//keyRings//cryptoKeys/); several Keys may share a
+// KeyID to emulate a rotated key with multiple versions.
+type Key struct {
+ KeyType keystore.KeyType
+ KeyID string
+ PrivateKey internal.Raw
+
+ // VersionNumber is the CryptoKeyVersion number. Defaults to 1.
+ VersionNumber uint64
+ // State is the version state. Defaults to ENABLED.
+ State kmspb.CryptoKeyVersion_CryptoKeyVersionState
+ // Algorithm is the algorithm reported for this version. Defaults to the algorithm matching
+ // KeyType; set it to emulate an unsupported one.
+ Algorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm
+}
+
+// FakeGCPKMSClient is an in-memory implementation of Client for tests. It emulates the parts of
+// Google Cloud KMS that the keystore uses, producing the same wire formats (PEM SPKI public keys,
+// DER ECDSA signatures, raw Ed25519 signatures) and the same resource-naming rules: asymmetric
+// operations only accept CryptoKeyVersion names, and CryptoKeys never report a primary version.
+type FakeGCPKMSClient struct {
+ keys []Key
+ createdAt time.Time
+
+ // CorruptPublicKeyCrc reports a wrong PemCrc32C from GetPublicKey so the keystore's
+ // CRC32C integrity check fails.
+ CorruptPublicKeyCrc bool
+ // OmitPublicKeyCrc omits PemCrc32C from GetPublicKey so the keystore treats the response
+ // as having no checksum.
+ OmitPublicKeyCrc bool
+ // CorruptSignatureCrc reports a wrong SignatureCrc32C from AsymmetricSign so the keystore's
+ // CRC32C integrity check fails.
+ CorruptSignatureCrc bool
+ // OmitSignatureCrc omits SignatureCrc32C from AsymmetricSign so the keystore treats the
+ // response as having no checksum.
+ OmitSignatureCrc bool
+ // SkipVerifiedDigestCrc32C reports VerifiedDigestCrc32C=false on ECDSA AsymmetricSign
+ // responses, exercising the keystore's rejection of an unverified digest.
+ SkipVerifiedDigestCrc32C bool
+ // SkipVerifiedDataCrc32C reports VerifiedDataCrc32C=false on Ed25519 AsymmetricSign
+ // responses, exercising the keystore's rejection of an unverified data payload.
+ SkipVerifiedDataCrc32C bool
+}
+
+// crc32cField builds the *_crc32c integrity field, honoring the fake's corruption/omission knobs.
+func (m *FakeGCPKMSClient) crc32cField(correct int64, corrupt, omit bool) *wrapperspb.Int64Value {
+ if omit {
+ return nil
+ }
+ value := correct
+ if corrupt {
+ value++
+ }
+ return wrapperspb.Int64(value)
+}
+
+func NewFakeGCPKMSClient(keys []Key) (*FakeGCPKMSClient, error) {
+ keys = append([]Key(nil), keys...)
+ for i := range keys {
+ if err := normalizeKey(&keys[i]); err != nil {
+ return nil, err
+ }
+ }
+ return &FakeGCPKMSClient{
+ keys: keys,
+ createdAt: time.Now(),
+ }, nil
+}
+
+// AddVersion appends a CryptoKeyVersion after construction, emulating a rotation that lands while
+// the keystore is live. Not safe for concurrent use with the client's read methods.
+func (m *FakeGCPKMSClient) AddVersion(key Key) error {
+ if err := normalizeKey(&key); err != nil {
+ return err
+ }
+ if _, err := m.findVersion(key.versionName()); err == nil {
+ return fmt.Errorf("version %s already exists", key.versionName())
+ }
+ m.keys = append(m.keys, key)
+ return nil
+}
+
+// normalizeKey validates a Key and fills in the fields a test left at their zero value.
+func normalizeKey(key *Key) error {
+ if key.KeyID == "" {
+ return errors.New("key ID is required")
+ }
+ if key.VersionNumber == 0 {
+ key.VersionNumber = 1
+ }
+ if key.State == kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_STATE_UNSPECIFIED {
+ key.State = kmspb.CryptoKeyVersion_ENABLED
+ }
+ if key.Algorithm == kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED {
+ algorithm, err := keyTypeToAlgorithm(key.KeyType)
+ if err != nil {
+ return err
+ }
+ key.Algorithm = algorithm
+ }
+ return nil
+}
+
+func keyTypeToAlgorithm(keyType keystore.KeyType) (kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm, error) {
+ switch keyType {
+ case keystore.ECDSA_S256:
+ return kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256, nil
+ case keystore.Ed25519:
+ return kmspb.CryptoKeyVersion_EC_SIGN_ED25519, nil
+ default:
+ return 0, fmt.Errorf("unsupported key type: %s", keyType)
+ }
+}
+
+// versionName returns the CryptoKeyVersion resource name of a key.
+func (k Key) versionName() string {
+ return k.KeyID + cryptoKeyVersionsSegment + strconv.FormatUint(k.VersionNumber, 10)
+}
+
+func (m *FakeGCPKMSClient) toCryptoKeyVersion(key *Key) *kmspb.CryptoKeyVersion {
+ return &kmspb.CryptoKeyVersion{
+ Name: key.versionName(),
+ Algorithm: key.Algorithm,
+ State: key.State,
+ CreateTime: timestamppb.New(m.createdAt),
+ }
+}
+
+// findVersion looks up a key by its CryptoKeyVersion resource name. Cloud KMS rejects a bare
+// CryptoKey name on the asymmetric endpoints, so the fake does too.
+func (m *FakeGCPKMSClient) findVersion(versionName string) (*Key, error) {
+ if !strings.Contains(versionName, cryptoKeyVersionsSegment) {
+ return nil, fmt.Errorf("%q is not a CryptoKeyVersion resource name", versionName)
+ }
+ for i := range m.keys {
+ if m.keys[i].versionName() == versionName {
+ return &m.keys[i], nil
+ }
+ }
+ return nil, errors.New("key not found")
+}
+
+func (m *FakeGCPKMSClient) GetCryptoKeyVersion(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) {
+ if req.Name == "" {
+ return nil, errors.New("key version name is required")
+ }
+ key, err := m.findVersion(req.Name)
+ if err != nil {
+ return nil, err
+ }
+ return m.toCryptoKeyVersion(key), nil
+}
+
+func (m *FakeGCPKMSClient) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) {
+ if req.Name == "" {
+ return nil, errors.New("key version name is required")
+ }
+ key, err := m.findVersion(req.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ var derPubKey []byte
+ switch key.KeyType {
+ case keystore.ECDSA_S256:
+ ecdsaKey, err := crypto.ToECDSA(internal.Bytes(key.PrivateKey))
+ if err != nil {
+ return nil, err
+ }
+ derPubKey, err = kms.SEC1ToASN1PublicKey(crypto.FromECDSAPub(&ecdsaKey.PublicKey))
+ if err != nil {
+ return nil, err
+ }
+ case keystore.Ed25519:
+ ed25519PrivKey, err := ed25519PrivateKey(key)
+ if err != nil {
+ return nil, err
+ }
+ pubKey := ed25519PrivKey.Public().(ed25519.PublicKey)
+ derPubKey, err = x509.MarshalPKIXPublicKey(pubKey)
+ if err != nil {
+ return nil, err
+ }
+ default:
+ return nil, fmt.Errorf("unsupported key type: %s", key.KeyType)
+ }
+
+ pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: derPubKey})
+ return &kmspb.PublicKey{
+ Name: req.Name,
+ Algorithm: key.Algorithm,
+ Pem: string(pemBytes),
+ PemCrc32C: m.crc32cField(crc32c(pemBytes), m.CorruptPublicKeyCrc, m.OmitPublicKeyCrc),
+ PublicKeyFormat: kmspb.PublicKey_PEM,
+ }, nil
+}
+
+func (m *FakeGCPKMSClient) AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) {
+ if req.Name == "" {
+ return nil, errors.New("key version name is required")
+ }
+ key, err := m.findVersion(req.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ switch key.KeyType {
+ case keystore.ECDSA_S256:
+ if req.Digest == nil {
+ return nil, errors.New("digest is required for ECDSA signing")
+ }
+ ecdsaKey, err := crypto.ToECDSA(internal.Bytes(key.PrivateKey))
+ if err != nil {
+ return nil, err
+ }
+ sec1Sig, err := crypto.Sign(req.Digest.GetSha256(), ecdsaKey)
+ if err != nil {
+ return nil, err
+ }
+ derSig, err := kms.SEC1ToASN1Sig(sec1Sig)
+ if err != nil {
+ return nil, err
+ }
+ return &kmspb.AsymmetricSignResponse{
+ Name: req.Name,
+ Signature: derSig,
+ SignatureCrc32C: m.crc32cField(crc32c(derSig), m.CorruptSignatureCrc, m.OmitSignatureCrc),
+ VerifiedDigestCrc32C: !m.SkipVerifiedDigestCrc32C,
+ }, nil
+ case keystore.Ed25519:
+ ed25519PrivKey, err := ed25519PrivateKey(key)
+ if err != nil {
+ return nil, err
+ }
+ signature := ed25519.Sign(ed25519PrivKey, req.Data)
+ return &kmspb.AsymmetricSignResponse{
+ Name: req.Name,
+ Signature: signature,
+ SignatureCrc32C: m.crc32cField(crc32c(signature), m.CorruptSignatureCrc, m.OmitSignatureCrc),
+ VerifiedDataCrc32C: !m.SkipVerifiedDataCrc32C,
+ }, nil
+ default:
+ return nil, fmt.Errorf("unsupported key type: %s", key.KeyType)
+ }
+}
+
+// ed25519PrivateKey returns the key's Ed25519 private key, erroring rather than letting the
+// crypto/ed25519 helpers panic on a wrong-sized key.
+func ed25519PrivateKey(key *Key) (ed25519.PrivateKey, error) {
+ privKey := ed25519.PrivateKey(internal.Bytes(key.PrivateKey))
+ if len(privKey) != ed25519.PrivateKeySize {
+ return nil, fmt.Errorf("invalid Ed25519 private key length: expected %d bytes, got %d", ed25519.PrivateKeySize, len(privKey))
+ }
+ return privKey, nil
+}
+
+func (m *FakeGCPKMSClient) Close() error {
+ return nil
+}
diff --git a/keystore/gcpkms/keystore.go b/keystore/gcpkms/keystore.go
new file mode 100644
index 0000000000..20512f23ac
--- /dev/null
+++ b/keystore/gcpkms/keystore.go
@@ -0,0 +1,279 @@
+package gcpkms
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "hash/crc32"
+ "sort"
+
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+
+ "github.com/smartcontractkit/chainlink-common/keystore"
+ "github.com/smartcontractkit/chainlink-common/keystore/kms"
+)
+
+// errUnsupportedAlgorithm marks a CryptoKeyVersion whose algorithm this keystore cannot use.
+var errUnsupportedAlgorithm = errors.New("unsupported Cloud KMS key algorithm")
+
+// castagnoliTable is the CRC32C (Castagnoli) table used by Google Cloud KMS for integrity checks.
+var castagnoliTable = crc32.MakeTable(crc32.Castagnoli)
+
+// crc32c returns the CRC32C checksum of data, matching the value Google Cloud KMS uses in its
+// *_crc32c integrity fields.
+func crc32c(data []byte) int64 {
+ return int64(crc32.Checksum(data, castagnoliTable))
+}
+
+// checkCrc32c verifies that a received CRC32C checksum matches the computed value of the data.
+//
+// Cloud KMS documents this as the required client-side step for detecting corruption in transit:
+// "you should verify the integrity of the response" by recomputing the CRC32C over the returned
+// bytes and comparing it against the response's *_crc32c field.
+// https://cloud.google.com/kms/docs/data-integrity-guidelines
+//
+// A missing checksum is treated as a failure: every response we check carries one, so its absence
+// means the response was truncated or tampered with in transit.
+func checkCrc32c(data []byte, received *wrapperspb.Int64Value) error {
+ if received == nil {
+ return errors.New("CRC32C integrity check failed: response is missing its checksum")
+ }
+ if want := crc32c(data); want != received.Value {
+ return fmt.Errorf("CRC32C integrity check failed: computed %d, received %d", want, received.Value)
+ }
+ return nil
+}
+
+type keystoreSignerReader struct {
+ client Client
+}
+
+// NewKeystore wraps a Cloud KMS client as a keystore.Reader and keystore.Signer.
+//
+// Unlike the file/DB and AWS KMS backends, this keystore does not support listing key rings:
+// GetKeys requires every key name to be provided explicitly (each a CryptoKeyVersion resource
+// name) and returns an error if none are. Callers that expect the documented "GetKeys returns all keys when
+// no names are provided" behavior (e.g. keystore.CoreKeystore.Accounts) must be configured with
+// explicit key names before wiring them to a GCP-backed keystore.
+func NewKeystore(client Client) (interface {
+ keystore.Reader
+ keystore.Signer
+}, error) {
+ if client == nil {
+ return nil, errors.New("GCP KMS client is required")
+ }
+ return &keystoreSignerReader{client: client}, nil
+}
+
+// cryptoKeyVersionAlgorithmToKeyType converts a Cloud KMS CryptoKeyVersionAlgorithm to a keystore
+// KeyType. Google Cloud KMS supports:
+// - EC_SIGN_SECP256K1_SHA256 (secp256k1) -> ECDSA_S256
+// - EC_SIGN_ED25519 (Ed25519) -> Ed25519
+func cryptoKeyVersionAlgorithmToKeyType(algo kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm) (keystore.KeyType, error) {
+ switch algo {
+ case kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256:
+ return keystore.ECDSA_S256, nil
+ case kmspb.CryptoKeyVersion_EC_SIGN_ED25519:
+ return keystore.Ed25519, nil
+ default:
+ return "", fmt.Errorf("%w: %s (supported: EC_SIGN_SECP256K1_SHA256, EC_SIGN_ED25519)", errUnsupportedAlgorithm, algo)
+ }
+}
+
+// getKeyVersion fetches a CryptoKeyVersion from Cloud KMS and validates that it is enabled and
+// uses a supported algorithm.
+func (k *keystoreSignerReader) getKeyVersion(ctx context.Context, versionName string) (*kmspb.CryptoKeyVersion, keystore.KeyType, error) {
+ version, err := k.client.GetCryptoKeyVersion(ctx, &kmspb.GetCryptoKeyVersionRequest{Name: versionName})
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to get crypto key version %s: %w", versionName, err)
+ }
+ if version == nil {
+ return nil, "", fmt.Errorf("empty crypto key version response from Cloud KMS for %s", versionName)
+ }
+ if version.Name != versionName {
+ return nil, "", fmt.Errorf("crypto key version response has name %q, expected %q", version.Name, versionName)
+ }
+ if version.State != kmspb.CryptoKeyVersion_ENABLED {
+ return nil, "", fmt.Errorf("crypto key version %s is not enabled (state=%s)", version.Name, version.State)
+ }
+ if version.CreateTime == nil {
+ return nil, "", fmt.Errorf("crypto key version %s has no creation time", version.Name)
+ }
+ keyType, err := cryptoKeyVersionAlgorithmToKeyType(version.Algorithm)
+ if err != nil {
+ return nil, "", fmt.Errorf("crypto key version %s: %w", version.Name, err)
+ }
+ return version, keyType, nil
+}
+
+// publicKeyBytes fetches the public key for a crypto key version and converts it to the
+// keystore's native format for the given key type.
+func (k *keystoreSignerReader) publicKeyBytes(ctx context.Context, versionName string, keyType keystore.KeyType) ([]byte, error) {
+ pk, err := k.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: versionName})
+ if err != nil {
+ return nil, fmt.Errorf("failed to get public key for %s: %w", versionName, err)
+ }
+ if pk == nil {
+ return nil, fmt.Errorf("empty public key response from Cloud KMS for %s", versionName)
+ }
+ if pk.Name != versionName {
+ return nil, fmt.Errorf("public key response has name %q, expected %q", pk.Name, versionName)
+ }
+ if err = checkCrc32c([]byte(pk.Pem), pk.PemCrc32C); err != nil {
+ return nil, fmt.Errorf("public key for %s: %w", versionName, err)
+ }
+
+ block, _ := pem.Decode([]byte(pk.Pem))
+ if block == nil {
+ return nil, fmt.Errorf("failed to decode PEM public key for %s", versionName)
+ }
+
+ switch keyType {
+ case keystore.ECDSA_S256:
+ // GCP returns the public key in ASN.1 DER-encoded SubjectPublicKeyInfo (SPKI) format,
+ // identical to AWS. Reuse the shared conversion.
+ return kms.ASN1ToSEC1PublicKey(block.Bytes)
+ case keystore.Ed25519:
+ pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert Ed25519 public key for %s: %w", versionName, err)
+ }
+ ed25519PubKey, ok := pubKey.(ed25519.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("failed to convert Ed25519 public key for %s to ed25519.PublicKey", versionName)
+ }
+ return ed25519PubKey, nil
+ default:
+ return nil, fmt.Errorf("unsupported key type: %s", keyType)
+ }
+}
+
+// GetKeys returns the requested keys from the Cloud KMS keystore, sorted by name.
+//
+// Key names are CryptoKeyVersion resource names
+// (projects//locations//keyRings//cryptoKeys//cryptoKeyVersions/): a key name
+// always names exactly one version, so rotating a key means configuring the new version's name.
+//
+// This deviates from keystore.Reader's documented contract ("GetKeys returns all keys in the
+// keystore if no names are provided"): this backend does not list key rings, so an empty
+// KeyNames request errors. See [NewKeystore].
+func (k *keystoreSignerReader) GetKeys(ctx context.Context, req keystore.GetKeysRequest) (keystore.GetKeysResponse, error) {
+ if len(req.KeyNames) == 0 {
+ return keystore.GetKeysResponse{}, errors.New("key names are required: this keystore does not list key rings")
+ }
+ versionNames := append([]string(nil), req.KeyNames...)
+ sort.Strings(versionNames)
+
+ keys := make([]keystore.GetKeyResponse, 0, len(versionNames))
+ seen := make(map[string]struct{}, len(versionNames))
+ for _, versionName := range versionNames {
+ if _, ok := seen[versionName]; ok {
+ return keystore.GetKeysResponse{}, fmt.Errorf("key %s provided multiple times", versionName)
+ }
+ seen[versionName] = struct{}{}
+ version, keyType, err := k.getKeyVersion(ctx, versionName)
+ if err != nil {
+ return keystore.GetKeysResponse{}, err
+ }
+ publicKey, err := k.publicKeyBytes(ctx, versionName, keyType)
+ if err != nil {
+ return keystore.GetKeysResponse{}, err
+ }
+ keys = append(keys, keystore.GetKeyResponse{
+ KeyInfo: keystore.NewKeyInfo(versionName, keyType, version.CreateTime.AsTime(), publicKey, []byte{}),
+ })
+ }
+ return keystore.GetKeysResponse{Keys: keys}, nil
+}
+
+// Sign signs data using the Cloud KMS crypto key version specified by the key name.
+//
+// The key name must be a CryptoKeyVersion resource name: Cloud KMS rejects bare CryptoKey names
+// on AsymmetricSign, so a rotation can never change which version a configured name signs with.
+func (k *keystoreSignerReader) Sign(ctx context.Context, req keystore.SignRequest) (keystore.SignResponse, error) {
+ _, keyType, err := k.getKeyVersion(ctx, req.KeyName)
+ if err != nil {
+ return keystore.SignResponse{}, err
+ }
+ versionName := req.KeyName
+
+ switch keyType {
+ case keystore.ECDSA_S256:
+ if len(req.Data) != 32 {
+ return keystore.SignResponse{}, fmt.Errorf("data must be 32 bytes for ECDSA_S256, got %d: %w", len(req.Data), keystore.ErrInvalidSignRequest)
+ }
+ // Needed to recover the SEC1 `v` byte from the ASN.1 signature below.
+ pubKeyBytes, err := k.publicKeyBytes(ctx, versionName, keyType)
+ if err != nil {
+ return keystore.SignResponse{}, fmt.Errorf("failed to get public key for key %s: %w", req.KeyName, err)
+ }
+
+ // The data is a pre-hashed 32-byte digest. For EC_SIGN_SECP256K1_SHA256 the digest field is
+ // the exact bytes signed; Cloud KMS does not re-hash.
+ sig, err := k.client.AsymmetricSign(ctx, &kmspb.AsymmetricSignRequest{
+ Name: versionName,
+ Digest: &kmspb.Digest{Digest: &kmspb.Digest_Sha256{Sha256: req.Data}},
+ DigestCrc32C: wrapperspb.Int64(crc32c(req.Data)),
+ })
+ if err != nil {
+ return keystore.SignResponse{}, fmt.Errorf("failed to sign data: %w", err)
+ }
+ if sig == nil {
+ return keystore.SignResponse{}, errors.New("empty signing response from Cloud KMS")
+ }
+ if sig.Name != versionName {
+ return keystore.SignResponse{}, fmt.Errorf("signing response has name %q, expected %q", sig.Name, versionName)
+ }
+ if !sig.VerifiedDigestCrc32C {
+ return keystore.SignResponse{}, errors.New("digest CRC32C checksum was not verified by Cloud KMS")
+ }
+ if err = checkCrc32c(sig.Signature, sig.SignatureCrc32C); err != nil {
+ return keystore.SignResponse{}, fmt.Errorf("signature for key %s: %w", req.KeyName, err)
+ }
+ // Cloud KMS returns the ECDSA signature in ASN.1 DER format, identical to AWS. Reuse the
+ // shared conversion to SEC1 (R || S || V).
+ signature, err := kms.ASN1ToSEC1Sig(sig.Signature, pubKeyBytes, req.Data)
+ if err != nil {
+ return keystore.SignResponse{}, fmt.Errorf("failed to convert Cloud KMS signature to SEC1 signature: %w", err)
+ }
+ return keystore.SignResponse{Signature: signature}, nil
+ case keystore.Ed25519:
+ // Ed25519 signs arbitrary length messages. For EC_SIGN_ED25519 the raw data field is the
+ // exact bytes signed; Cloud KMS does not hash.
+ sig, err := k.client.AsymmetricSign(ctx, &kmspb.AsymmetricSignRequest{
+ Name: versionName,
+ Data: req.Data,
+ DataCrc32C: wrapperspb.Int64(crc32c(req.Data)),
+ })
+ if err != nil {
+ return keystore.SignResponse{}, fmt.Errorf("failed to sign data: %w", err)
+ }
+ if sig == nil {
+ return keystore.SignResponse{}, errors.New("empty signing response from Cloud KMS")
+ }
+ if sig.Name != versionName {
+ return keystore.SignResponse{}, fmt.Errorf("signing response has name %q, expected %q", sig.Name, versionName)
+ }
+ if !sig.VerifiedDataCrc32C {
+ return keystore.SignResponse{}, errors.New("data CRC32C checksum was not verified by Cloud KMS")
+ }
+ if err = checkCrc32c(sig.Signature, sig.SignatureCrc32C); err != nil {
+ return keystore.SignResponse{}, fmt.Errorf("signature for key %s: %w", req.KeyName, err)
+ }
+ // Ed25519 signatures from Cloud KMS are already in the correct format.
+ if len(sig.Signature) != ed25519.SignatureSize {
+ return keystore.SignResponse{}, fmt.Errorf("invalid Ed25519 signature length: expected %d bytes, got %d", ed25519.SignatureSize, len(sig.Signature))
+ }
+ return keystore.SignResponse{Signature: sig.Signature}, nil
+ default:
+ return keystore.SignResponse{}, fmt.Errorf("key %s: %w", req.KeyName, keystore.ErrInvalidSignRequest)
+ }
+}
+
+func (k *keystoreSignerReader) Verify(ctx context.Context, req keystore.VerifyRequest) (keystore.VerifyResponse, error) {
+ return keystore.Verify(ctx, req)
+}
diff --git a/keystore/gcpkms/keystore_test.go b/keystore/gcpkms/keystore_test.go
new file mode 100644
index 0000000000..f3e90ec1ab
--- /dev/null
+++ b/keystore/gcpkms/keystore_test.go
@@ -0,0 +1,364 @@
+package gcpkms_test
+
+import (
+ "crypto/ed25519"
+ "testing"
+
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/stretchr/testify/require"
+
+ "github.com/smartcontractkit/chainlink-common/keystore"
+ gcpkms "github.com/smartcontractkit/chainlink-common/keystore/gcpkms"
+ "github.com/smartcontractkit/chainlink-common/keystore/internal"
+)
+
+const (
+ keyRingName = "projects/test-project/locations/us-central1/keyRings/test-ring"
+ keyName = keyRingName + "/cryptoKeys/test-key"
+ keyName2 = keyRingName + "/cryptoKeys/test-key-2"
+
+ // Key names are CryptoKeyVersion resource names: a name always names exactly one version.
+ keyVersion1 = keyName + "/cryptoKeyVersions/1"
+ key2Version1 = keyName2 + "/cryptoKeyVersions/1"
+)
+
+func TestGCPKMSKeystore(t *testing.T) {
+ key, err := crypto.GenerateKey()
+ require.NoError(t, err)
+ key2, err := crypto.GenerateKey()
+ require.NoError(t, err)
+ fakeClient, err := gcpkms.NewFakeGCPKMSClient([]gcpkms.Key{
+ {
+ KeyType: keystore.ECDSA_S256,
+ PrivateKey: internal.NewRaw(crypto.FromECDSA(key)),
+ KeyID: keyName,
+ },
+ {
+ KeyType: keystore.ECDSA_S256,
+ PrivateKey: internal.NewRaw(crypto.FromECDSA(key2)),
+ KeyID: keyName2,
+ },
+ })
+ require.NoError(t, err)
+ ks, err := gcpkms.NewKeystore(fakeClient)
+ require.NoError(t, err)
+ ctx := t.Context()
+
+ t.Run("GetKeys", func(t *testing.T) {
+ t.Run("no key names is rejected", func(t *testing.T) {
+ _, err := ks.GetKeys(ctx, keystore.GetKeysRequest{})
+ require.ErrorContains(t, err, "does not list key rings")
+ })
+ t.Run("specific keys are sorted by name", func(t *testing.T) {
+ resp, err := ks.GetKeys(ctx, keystore.GetKeysRequest{
+ KeyNames: []string{keyVersion1, key2Version1},
+ })
+ require.NoError(t, err)
+ require.Len(t, resp.Keys, 2)
+ require.Equal(t, key2Version1, resp.Keys[0].KeyInfo.Name)
+ require.Equal(t, keyVersion1, resp.Keys[1].KeyInfo.Name)
+ require.Equal(t, keystore.ECDSA_S256, resp.Keys[1].KeyInfo.KeyType)
+ require.Equal(t, crypto.FromECDSAPub(&key.PublicKey), resp.Keys[1].KeyInfo.PublicKey)
+ })
+ t.Run("no such key", func(t *testing.T) {
+ _, err := ks.GetKeys(ctx, keystore.GetKeysRequest{
+ KeyNames: []string{"projects/p/locations/l/keyRings/r/cryptoKeys/nope/cryptoKeyVersions/1"},
+ })
+ require.Error(t, err)
+ })
+ t.Run("bare crypto key name is rejected", func(t *testing.T) {
+ _, err := ks.GetKeys(ctx, keystore.GetKeysRequest{
+ KeyNames: []string{keyName},
+ })
+ require.ErrorContains(t, err, "not a CryptoKeyVersion resource name")
+ })
+ })
+
+ t.Run("SignVerify", func(t *testing.T) {
+ t.Run("invalid sign request", func(t *testing.T) {
+ _, err := ks.Sign(ctx, keystore.SignRequest{
+ KeyName: keyVersion1,
+ Data: make([]byte, 31), // 31 byte digest
+ })
+ require.Error(t, err)
+ require.ErrorIs(t, err, keystore.ErrInvalidSignRequest)
+ })
+ t.Run("no such key", func(t *testing.T) {
+ _, err := ks.Sign(ctx, keystore.SignRequest{
+ KeyName: "projects/p/locations/l/keyRings/r/cryptoKeys/nope/cryptoKeyVersions/1",
+ Data: make([]byte, 32), // 32 byte digest
+ })
+ require.Error(t, err)
+ })
+ t.Run("bare crypto key name is rejected", func(t *testing.T) {
+ _, err := ks.Sign(ctx, keystore.SignRequest{
+ KeyName: keyName,
+ Data: make([]byte, 32),
+ })
+ require.ErrorContains(t, err, "not a CryptoKeyVersion resource name")
+ })
+ t.Run("success", func(t *testing.T) {
+ signResp, err := ks.Sign(ctx, keystore.SignRequest{
+ KeyName: keyVersion1,
+ Data: make([]byte, 32), // 32 byte digest
+ })
+ require.NoError(t, err)
+ require.NotNil(t, signResp.Signature)
+ verifyResp, err := ks.Verify(ctx, keystore.VerifyRequest{
+ KeyType: keystore.ECDSA_S256,
+ PublicKey: crypto.FromECDSAPub(&key.PublicKey),
+ Data: make([]byte, 32), // 32 byte digest
+ Signature: signResp.Signature,
+ })
+ require.NoError(t, err)
+ require.True(t, verifyResp.Valid)
+ })
+ })
+}
+
+func TestGCPKMSKeystore_Ed25519(t *testing.T) {
+ _, ed25519PrivKey, err := ed25519.GenerateKey(nil)
+ require.NoError(t, err)
+ ed25519PubKey := ed25519PrivKey.Public().(ed25519.PublicKey)
+
+ fakeClient, err := gcpkms.NewFakeGCPKMSClient([]gcpkms.Key{
+ {
+ KeyType: keystore.Ed25519,
+ KeyID: keyName,
+ PrivateKey: internal.NewRaw(ed25519PrivKey),
+ },
+ })
+ require.NoError(t, err)
+ ks, err := gcpkms.NewKeystore(fakeClient)
+ require.NoError(t, err)
+ ctx := t.Context()
+
+ t.Run("GetKeys", func(t *testing.T) {
+ resp, err := ks.GetKeys(ctx, keystore.GetKeysRequest{
+ KeyNames: []string{keyVersion1},
+ })
+ require.NoError(t, err)
+ require.Len(t, resp.Keys, 1)
+ require.Equal(t, keyVersion1, resp.Keys[0].KeyInfo.Name)
+ require.Equal(t, keystore.Ed25519, resp.Keys[0].KeyInfo.KeyType)
+ require.Equal(t, []byte(ed25519PubKey), resp.Keys[0].KeyInfo.PublicKey)
+ })
+
+ t.Run("SignVerify", func(t *testing.T) {
+ // Ed25519 can sign arbitrary length messages
+ testData := []byte("hello, world")
+ signResp, err := ks.Sign(ctx, keystore.SignRequest{
+ KeyName: keyVersion1,
+ Data: testData,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, signResp.Signature)
+ require.Len(t, signResp.Signature, 64) // Ed25519 signatures are 64 bytes
+
+ verifyResp, err := ks.Verify(ctx, keystore.VerifyRequest{
+ KeyType: keystore.Ed25519,
+ PublicKey: ed25519PubKey,
+ Data: testData,
+ Signature: signResp.Signature,
+ })
+ require.NoError(t, err)
+ require.True(t, verifyResp.Valid)
+ })
+}
+
+// Requesting a version this keystore cannot use e.g. an unsupported algorithm or a disabled
+// version surfaces the error from both GetKeys and Sign.
+func TestGCPKMSKeystore_UnusableVersions(t *testing.T) {
+ p256Key, err := crypto.GenerateKey()
+ require.NoError(t, err)
+ disabledKey, err := crypto.GenerateKey()
+ require.NoError(t, err)
+
+ fakeClient, err := gcpkms.NewFakeGCPKMSClient([]gcpkms.Key{
+ {
+ // A signing key on a curve this keystore does not support.
+ KeyType: keystore.ECDSA_S256,
+ KeyID: keyName,
+ Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256,
+ PrivateKey: internal.NewRaw(crypto.FromECDSA(p256Key)),
+ },
+ {
+ KeyType: keystore.ECDSA_S256,
+ KeyID: keyName2,
+ State: kmspb.CryptoKeyVersion_DISABLED,
+ PrivateKey: internal.NewRaw(crypto.FromECDSA(disabledKey)),
+ },
+ })
+ require.NoError(t, err)
+ ks, err := gcpkms.NewKeystore(fakeClient)
+ require.NoError(t, err)
+ ctx := t.Context()
+
+ _, err = ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: []string{keyVersion1}})
+ require.ErrorContains(t, err, "unsupported Cloud KMS key algorithm")
+ _, err = ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: make([]byte, 32)})
+ require.ErrorContains(t, err, "unsupported Cloud KMS key algorithm")
+
+ _, err = ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: []string{key2Version1}})
+ require.ErrorContains(t, err, "is not enabled")
+ _, err = ks.Sign(ctx, keystore.SignRequest{KeyName: key2Version1, Data: make([]byte, 32)})
+ require.ErrorContains(t, err, "is not enabled")
+}
+
+func TestGCPKMSKeystore_InvalidEd25519Key(t *testing.T) {
+ fakeClient, err := gcpkms.NewFakeGCPKMSClient([]gcpkms.Key{
+ {
+ KeyType: keystore.Ed25519,
+ KeyID: keyName,
+ PrivateKey: internal.NewRaw(make([]byte, ed25519.SeedSize)), // seed, not a private key
+ },
+ })
+ require.NoError(t, err)
+ ks, err := gcpkms.NewKeystore(fakeClient)
+ require.NoError(t, err)
+
+ // Must error rather than panic inside crypto/ed25519.
+ _, err = ks.Sign(t.Context(), keystore.SignRequest{KeyName: keyVersion1, Data: []byte("hello")})
+ require.ErrorContains(t, err, "invalid Ed25519 private key length")
+}
+
+// A CryptoKeyVersion name names one version forever, so a rotation can never change which key a
+// configured name signs with: the public key a caller already holds stays the one that verifies
+// its signatures. Adopting a rotation means configuring the new version's name.
+func TestGCPKMSKeystore_Rotation(t *testing.T) {
+ originalKey, err := crypto.GenerateKey()
+ require.NoError(t, err)
+ rotatedKey, err := crypto.GenerateKey()
+ require.NoError(t, err)
+
+ fakeClient, err := gcpkms.NewFakeGCPKMSClient([]gcpkms.Key{
+ {KeyType: keystore.ECDSA_S256, KeyID: keyName, VersionNumber: 1, PrivateKey: internal.NewRaw(crypto.FromECDSA(originalKey))},
+ })
+ require.NoError(t, err)
+ ks, err := gcpkms.NewKeystore(fakeClient)
+ require.NoError(t, err)
+ ctx := t.Context()
+
+ resp, err := ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: []string{keyVersion1}})
+ require.NoError(t, err)
+ require.Len(t, resp.Keys, 1)
+ publicKey := resp.Keys[0].KeyInfo.PublicKey
+ require.Equal(t, crypto.FromECDSAPub(&originalKey.PublicKey), publicKey)
+
+ // Cloud KMS gains a newer enabled version after the caller has read the public key.
+ require.NoError(t, fakeClient.AddVersion(gcpkms.Key{
+ KeyType: keystore.ECDSA_S256,
+ KeyID: keyName,
+ VersionNumber: 2,
+ PrivateKey: internal.NewRaw(crypto.FromECDSA(rotatedKey)),
+ }))
+
+ signResp, err := ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: make([]byte, 32)})
+ require.NoError(t, err)
+
+ verifyResp, err := ks.Verify(ctx, keystore.VerifyRequest{
+ KeyType: keystore.ECDSA_S256,
+ PublicKey: publicKey,
+ Data: make([]byte, 32),
+ Signature: signResp.Signature,
+ })
+ require.NoError(t, err)
+ require.True(t, verifyResp.Valid, "signature must verify against the public key GetKeys reported")
+
+ // GetKeys keeps reporting the configured version too, so the two never diverge.
+ resp, err = ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: []string{keyVersion1}})
+ require.NoError(t, err)
+ require.Equal(t, publicKey, resp.Keys[0].KeyInfo.PublicKey)
+}
+
+// TestGCPKMSKeystore_CRC32CIntegrityChecks exercises the keystore's defense-in-depth against
+// transport corruption: it must reject a public key or signature whose CRC32C checksum is missing
+// or wrong, and reject signatures Cloud KMS reports it did not verify.
+func TestGCPKMSKeystore_CRC32CIntegrityChecks(t *testing.T) {
+ ctx := t.Context()
+
+ key, err := crypto.GenerateKey()
+ require.NoError(t, err)
+
+ newECDSAKeystore := func(mutate func(*gcpkms.FakeGCPKMSClient)) interface {
+ keystore.Reader
+ keystore.Signer
+ } {
+ c, err := gcpkms.NewFakeGCPKMSClient([]gcpkms.Key{{
+ KeyType: keystore.ECDSA_S256,
+ KeyID: keyName,
+ PrivateKey: internal.NewRaw(crypto.FromECDSA(key)),
+ }})
+ require.NoError(t, err)
+ mutate(c)
+ ks, err := gcpkms.NewKeystore(c)
+ require.NoError(t, err)
+ return ks
+ }
+
+ t.Run("public key wrong checksum", func(t *testing.T) {
+ ks := newECDSAKeystore(func(c *gcpkms.FakeGCPKMSClient) { c.CorruptPublicKeyCrc = true })
+ _, err := ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: []string{keyVersion1}})
+ require.ErrorContains(t, err, "CRC32C integrity check failed")
+ })
+ t.Run("public key missing checksum", func(t *testing.T) {
+ ks := newECDSAKeystore(func(c *gcpkms.FakeGCPKMSClient) { c.OmitPublicKeyCrc = true })
+ _, err := ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: []string{keyVersion1}})
+ require.ErrorContains(t, err, "CRC32C integrity check failed")
+ })
+ t.Run("signature wrong checksum", func(t *testing.T) {
+ ks := newECDSAKeystore(func(c *gcpkms.FakeGCPKMSClient) { c.CorruptSignatureCrc = true })
+ _, err := ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: make([]byte, 32)})
+ require.ErrorContains(t, err, "CRC32C integrity check failed")
+ })
+ t.Run("signature missing checksum", func(t *testing.T) {
+ ks := newECDSAKeystore(func(c *gcpkms.FakeGCPKMSClient) { c.OmitSignatureCrc = true })
+ _, err := ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: make([]byte, 32)})
+ require.ErrorContains(t, err, "CRC32C integrity check failed")
+ })
+ t.Run("digest not verified by KMS", func(t *testing.T) {
+ ks := newECDSAKeystore(func(c *gcpkms.FakeGCPKMSClient) { c.SkipVerifiedDigestCrc32C = true })
+ _, err := ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: make([]byte, 32)})
+ require.ErrorContains(t, err, "digest CRC32C checksum was not verified")
+ })
+}
+
+func TestGCPKMSKeystore_Ed25519CRC32CIntegrityChecks(t *testing.T) {
+ ctx := t.Context()
+
+ _, privKey, err := ed25519.GenerateKey(nil)
+ require.NoError(t, err)
+
+ newEd25519Keystore := func(mutate func(*gcpkms.FakeGCPKMSClient)) interface {
+ keystore.Reader
+ keystore.Signer
+ } {
+ c, err := gcpkms.NewFakeGCPKMSClient([]gcpkms.Key{{
+ KeyType: keystore.Ed25519,
+ KeyID: keyName,
+ PrivateKey: internal.NewRaw(privKey),
+ }})
+ require.NoError(t, err)
+ mutate(c)
+ ks, err := gcpkms.NewKeystore(c)
+ require.NoError(t, err)
+ return ks
+ }
+
+ t.Run("signature wrong checksum", func(t *testing.T) {
+ ks := newEd25519Keystore(func(c *gcpkms.FakeGCPKMSClient) { c.CorruptSignatureCrc = true })
+ _, err := ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: []byte("hello")})
+ require.ErrorContains(t, err, "CRC32C integrity check failed")
+ })
+ t.Run("signature missing checksum", func(t *testing.T) {
+ ks := newEd25519Keystore(func(c *gcpkms.FakeGCPKMSClient) { c.OmitSignatureCrc = true })
+ _, err := ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: []byte("hello")})
+ require.ErrorContains(t, err, "CRC32C integrity check failed")
+ })
+ t.Run("data not verified by KMS", func(t *testing.T) {
+ ks := newEd25519Keystore(func(c *gcpkms.FakeGCPKMSClient) { c.SkipVerifiedDataCrc32C = true })
+ _, err := ks.Sign(ctx, keystore.SignRequest{KeyName: keyVersion1, Data: []byte("hello")})
+ require.ErrorContains(t, err, "data CRC32C checksum was not verified")
+ })
+}
diff --git a/keystore/go.mod b/keystore/go.mod
index e77fcc6ace..52045654ab 100644
--- a/keystore/go.mod
+++ b/keystore/go.mod
@@ -3,6 +3,7 @@ module github.com/smartcontractkit/chainlink-common/keystore
go 1.26.6
require (
+ cloud.google.com/go/kms v1.33.0
github.com/NethermindEth/juno v0.15.11
github.com/NethermindEth/starknet.go v0.17.1
github.com/aws/aws-sdk-go-v2 v1.41.1
@@ -14,6 +15,7 @@ require (
github.com/ethereum/go-ethereum v1.17.4
github.com/gagliardetto/solana-go v1.13.0
github.com/google/uuid v1.6.0
+ github.com/googleapis/gax-go/v2 v2.23.0
github.com/hdevalence/ed25519consensus v0.2.0
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
@@ -29,10 +31,17 @@ require (
go.dedis.ch/fixbuf v1.0.3
go.dedis.ch/kyber/v3 v3.1.0
golang.org/x/crypto v0.53.0
+ google.golang.org/api v0.287.1
google.golang.org/protobuf v1.36.11
)
require (
+ cloud.google.com/go v0.123.0 // indirect
+ cloud.google.com/go/auth v0.20.0 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
+ cloud.google.com/go/compute/metadata v0.9.0 // indirect
+ cloud.google.com/go/iam v1.11.0 // indirect
+ cloud.google.com/go/longrunning v1.2.0 // indirect
cosmossdk.io/api v0.7.6 // indirect
cosmossdk.io/collections v0.4.0 // indirect
cosmossdk.io/core v0.11.0 // indirect
@@ -99,6 +108,7 @@ require (
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
github.com/fatih/color v1.18.0 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fjl/jsonw v0.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
@@ -127,6 +137,8 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
+ github.com/google/s2a-go v0.1.9 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.17 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/graph-gophers/graphql-go v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
@@ -208,7 +220,8 @@ require (
go.mongodb.org/mongo-driver v1.17.7 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
@@ -231,19 +244,20 @@ require (
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
- golang.org/x/mod v0.36.0 // indirect
- golang.org/x/net v0.55.0 // indirect
+ golang.org/x/mod v0.37.0 // indirect
+ golang.org/x/net v0.56.0 // indirect
+ golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
- golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
+ golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect
golang.org/x/term v0.44.0 // indirect
- golang.org/x/text v0.38.0 // indirect
- golang.org/x/time v0.14.0 // indirect
- golang.org/x/tools v0.45.0 // indirect
+ golang.org/x/text v0.39.0 // indirect
+ golang.org/x/time v0.15.0 // indirect
+ golang.org/x/tools v0.47.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
- google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
+ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/grpc v1.83.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
diff --git a/keystore/go.sum b/keystore/go.sum
index 243ebc7b08..72b3c8283f 100644
--- a/keystore/go.sum
+++ b/keystore/go.sum
@@ -1,5 +1,19 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
+cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
+cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
+cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
+cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
+cloud.google.com/go/kms v1.33.0 h1:pG0X78m212b2pv9N4fdMoUO69LuZGQ9kSvn8sHBOFAo=
+cloud.google.com/go/kms v1.33.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA=
+cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
+cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY=
cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38=
cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s=
@@ -128,6 +142,8 @@ github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1/go.mod h1:6Q+F2
github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q=
github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
+github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
@@ -209,7 +225,12 @@ github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
+github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
+github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
+github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
@@ -350,9 +371,15 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us=
github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.17 h1:73NfMHdiqo9JFU9+7a5ExpVa10/R29pXfZIaW559nrg=
+github.com/googleapis/enterprise-certificate-proxy v0.3.17/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
+github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
+github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -572,6 +599,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -743,8 +772,10 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
@@ -821,8 +852,8 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
-golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
+golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -843,10 +874,12 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
-golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -890,8 +923,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
-golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
+golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc=
+golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
@@ -900,10 +933,10 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
-golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
-golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
-golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
+golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -914,8 +947,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
-golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
+golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -925,6 +958,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+google.golang.org/api v0.287.1 h1:LiyJx32VU3cwQfLchn/513qKhc25hq0pEANYJoWNnnI=
+google.golang.org/api v0.287.1/go.mod h1:lM2kYRzYUCBY91P9h6VF1PYmvhxii3O5hji37qRvIcY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -932,12 +967,12 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE=
-google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
-google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
+google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
+google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
+google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/keystore/kms/asn1_test.go b/keystore/kms/asn1_test.go
index f0407a6e70..9cb4d97a9f 100644
--- a/keystore/kms/asn1_test.go
+++ b/keystore/kms/asn1_test.go
@@ -1,6 +1,8 @@
package kms_test
import (
+ "math/big"
+ "slices"
"testing"
"github.com/ethereum/go-ethereum/crypto"
@@ -51,3 +53,47 @@ func TestASN1SignatureToSEC1Signature(t *testing.T) {
require.Len(t, sec1Sig, 65)
require.Equal(t, sig, sec1Sig)
}
+
+// TestASN1SignatureToSEC1SignatureHighS exercises the EIP-2 high-S normalization branch in
+// ASN1ToSEC1Sig. go-ethereum's crypto.Sign always returns a low-S signature (S <= N/2), so the
+// happy path never covers it. A non-normalizing signer such as Cloud KMS emits S > N/2 roughly
+// half the time, which is exactly the input this test builds: same R, S negated (S' = N - S).
+func TestASN1SignatureToSEC1SignatureHighS(t *testing.T) {
+ privateKey, err := crypto.GenerateKey()
+ require.NoError(t, err)
+ sec1PubKey := crypto.FromECDSAPub(&privateKey.PublicKey)
+
+ hash := crypto.Keccak256Hash([]byte("high-S test"))
+
+ // crypto.Sign normalizes S to <= N/2 (decred SignCompact). Confirm the assumption.
+ sig, err := crypto.Sign(hash[:], privateKey)
+ require.NoError(t, err)
+ require.Len(t, sig, 65)
+
+ n := crypto.S256().Params().N
+ halfN := new(big.Int).Div(n, big.NewInt(2))
+ s := new(big.Int).SetBytes(sig[32:64])
+ require.LessOrEqual(t, s.Cmp(halfN), 0, "crypto.Sign must produce a low-S signature")
+
+ // Negate S to obtain the high-S (S > N/2) value a non-normalizing signer would emit.
+ highS := new(big.Int).Sub(n, s)
+ require.Positive(t, highS.Cmp(halfN), "negating S must yield a high-S signature")
+
+ // Rebuild a SEC1 signature carrying the high S, then DER-encode it as Cloud KMS would.
+ highSSec1 := slices.Concat(sig[:32], highS.FillBytes(make([]byte, 32)), []byte{0})
+ highSDER, err := kms.SEC1ToASN1Sig(highSSec1)
+ require.NoError(t, err)
+
+ // ASN1ToSEC1Sig must flip S back below N/2 per EIP-2 and recover the correct V.
+ sec1Sig, err := kms.ASN1ToSEC1Sig(highSDER, sec1PubKey, hash[:])
+ require.NoError(t, err)
+ require.Len(t, sec1Sig, 65)
+
+ // The normalized signature must be byte-for-byte the original low-S signature (same R, S, V).
+ require.Equal(t, sig, sec1Sig)
+ require.Equal(t, sig[64], sec1Sig[64], "recovery id must match the low-S signature")
+
+ recovered, err := crypto.Ecrecover(hash[:], sec1Sig)
+ require.NoError(t, err)
+ require.Equal(t, sec1PubKey, recovered, "normalized high-S signature must recover the signer")
+}