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") +}