Skip to content

Add gcpkms keystore implementation - #2338

Open
AmrMohamedRezk wants to merge 4 commits into
mainfrom
gcp_kms
Open

Add gcpkms keystore implementation#2338
AmrMohamedRezk wants to merge 4 commits into
mainfrom
gcp_kms

Conversation

@AmrMohamedRezk

@AmrMohamedRezk AmrMohamedRezk commented Aug 24, 2026

Copy link
Copy Markdown

CCIP-13120 https://smartcontract-it.atlassian.net/browse/CCIP-13120

Add a Google Cloud KMS keystore backend

Adds keystore/gcpkms, a keystore.Reader + keystore.Signer implementation backed by Google Cloud KMS, alongside the existing AWS backend in keystore/kms.

Copilot AI lite review requested due to automatic review settings August 24, 2026 14:04
@AmrMohamedRezk
AmrMohamedRezk requested review from a team as code owners August 24, 2026 14:04
@github-actions

Copy link
Copy Markdown
Contributor

👋 AmrMohamedRezk, thanks for creating this pull request!

To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team.

Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks!

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

✅ API Diff Results - github.com/smartcontractkit/chainlink-common/keystore

✅ Compatible Changes (1)

package github (1)
  • com/smartcontractkit/chainlink-common/keystore/gcpkms — ➕ Added

📄 View full apidiff report

Comment thread keystore/reader.go Outdated
// Reader is the interface for reading keys from the keystore.
// GetKeys returns all keys in the keystore if no names are provided, or the keys with the given names.
// Keys are sorted by name in lexicographic order.
// The order of returned keys is implementation-specific; callers must not rely on a particular ordering.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm this seems like a breaking change, can we not satisfy this requirement (maybe sort client side)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that shouldn't break any existing behavior. The old AWS behavior is as is. The new behavior doesn't, this is just to make sure clients don't always assume it's lexicographic order regardless of the backend.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I mean - if previous clients of this interface assume the sortation is lexicographical, and they upgrade and they don't get lexicographical, wouldn't their code silently break?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Google Cloud KMS-backed keystore.Reader and keystore.Signer.

Changes:

  • Adds GCP KMS client, keystore, and in-memory fake.
  • Supports key discovery, version resolution, public-key retrieval, and signing.
  • Adds tests and updates dependencies and ordering documentation.

Review findings:

  • Critical (2 votes): AsymmetricSignResponse.Name is referenced in fake_client.go, causing compilation errors.
  • Critical (2 votes): keystore.go references the nonexistent AsymmetricSignResponse.Name field.
  • Moderate (2 votes): Signers using parent CryptoKey names may resolve a different version than the one used for retrieved metadata.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Summary
keystore/reader.go Updates key ordering documentation.
keystore/go.sum Records dependency checksums.
keystore/go.mod Adds Google Cloud dependencies.
keystore/gcpkms/keystore.go Implements GCP KMS key retrieval and signing.
keystore/gcpkms/keystore_test.go Tests GCP KMS behavior and edge cases.
keystore/gcpkms/fake_client.go Provides an in-memory KMS test client.
keystore/gcpkms/client.go Wraps the Google Cloud KMS SDK.
Suppressed comments (3)

keystore/gcpkms/client.go:97

  • ListCryptoKeyVersions also uses the unspecified BASIC view. The resolver requires each listed version's Algorithm and CreateTime, but BASIC responses omit those fields; a parent-key lookup therefore sees an unspecified algorithm and errors before it can sign or list the key. Request CryptoKeyVersionView_FULL here.
	iter := c.client.ListCryptoKeyVersions(ctx, &kmspb.ListCryptoKeyVersionsRequest{Parent: cryptoKeyName})

keystore/gcpkms/client.go:92

  • ListCryptoKeys is called with the default BASIC/unspecified view. Cloud KMS does not populate the metadata this implementation reads in signingKeyNames (Purpose and VersionTemplate) in that view; in a real key ring those fields remain zero/nil, so asymmetric signing keys can be skipped as non-signing. Request CryptoKeyView_FULL (or fetch the metadata explicitly) before relying on these fields.
	iter := c.client.ListCryptoKeys(ctx, &kmspb.ListCryptoKeysRequest{Parent: keyRingName})

keystore/gcpkms/keystore.go:305

  • Sorting only the discovery path makes this backend violate the Reader contract that existed before this PR (keystore/reader.go:27): an explicit allowlist is returned in caller order here, while the in-memory and AWS readers sort it lexicographically. Please preserve the shared API guarantee by sorting keyNames for both paths instead of weakening Reader's contract.
		// Cloud KMS does not guarantee a listing order; sort for a stable response.
		sort.Strings(keyNames)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +243 to +247
return &kmspb.AsymmetricSignResponse{
Name: req.Name,
Signature: derSig,
SignatureCrc32C: wrapperspb.Int64(crc32c(derSig)),
VerifiedDigestCrc32C: true,
Comment on lines +367 to +369
if sig.Name != versionName {
return keystore.SignResponse{}, fmt.Errorf("signing response has name %q, expected %q", sig.Name, versionName)
}
Comment on lines +326 to +329
createdAt := resolved.version.CreateTime.AsTime()
keys = append(keys, keystore.GetKeyResponse{
KeyInfo: keystore.NewKeyInfo(keyName, resolved.keyType, createdAt, publicKeyBytes, []byte{}),
})
Comment thread keystore/reader.go Outdated
// Reader is the interface for reading keys from the keystore.
// GetKeys returns all keys in the keystore if no names are provided, or the keys with the given names.
// Keys are sorted by name in lexicographic order.
// The order of returned keys is implementation-specific; callers must not rely on a particular ordering.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I mean - if previous clients of this interface assume the sortation is lexicographical, and they upgrade and they don't get lexicographical, wouldn't their code silently break?

Comment thread keystore/gcpkms/keystore.go Outdated
}

// checkCrc32c verifies that a received CRC32C checksum matches the computed value of the data.
// This is Google's recommended integrity check for responses returned by Cloud KMS. A missing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you link the docs where it mentions this? Would be useful for reviewers (and maybe auditors).

Comment thread keystore/gcpkms/keystore.go Outdated
return keystore.SignResponse{}, fmt.Errorf("signature for key %s: %w", req.KeyName, err)
}
// Ed25519 signatures from Cloud KMS are already in the correct format (64 bytes).
if len(sig.Signature) != 64 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can use ed25519.SignatureSize

Comment thread keystore/gcpkms/client.go Outdated
Comment on lines +54 to +57
// CredentialsFile is the path to a GCP service account JSON key. Local development only —
// leave empty in production, where credentials come from the default credential chain
// (GKE Workload Identity, GCE instance/service accounts, or GOOGLE_APPLICATION_CREDENTIALS).
CredentialsFile string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one could use GOOGLE_APPLICATION_CREDENTIALS for local dev, can we get rid of this credentials file?

Comment thread keystore/gcpkms/client.go Outdated
Comment on lines +66 to +70
// WithCredentialsFile is deprecated upstream because long-lived key files are a standing
// credential-leak risk. It is kept for the local-development path only; production leaves
// CredentialsFile empty and authenticates through Application Default Credentials.
//nolint:staticcheck // deliberate: local-development-only service-account key support
clientOpts = append(clientOpts, option.WithCredentialsFile(opts.CredentialsFile))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would remove this if its deprecated, and if an env var can be used instead. Those should be settable in devenv anyway via the testcontainers overrides

@jmank88
jmank88 requested a review from pavel-raykov August 25, 2026 12:37
Comment thread keystore/gcpkms/client.go Outdated
// ClientWithClose is the client returned by NewClient: the full Cloud KMS surface, including key
// ring listing, plus the underlying transport lifecycle. Whether listing actually succeeds is a
// matter of the credentials' IAM bindings, not of the Go type.
type ClientWithClose interface {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the sub-interfaces need to be defined separately? If so, can we use Client here and name the other one differently?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will drop the interface and replace it by an adapter.

if key.Purpose != kmspb.CryptoKey_ASYMMETRIC_SIGN {
continue
}
if template := key.VersionTemplate; template != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if the template is nil? Should the key also be discarded in this case?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, VersionTemplate is optional in the API. I will address.

client Client
keyRingName string

mu sync.RWMutex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need all these caching mechanism? Can't you include the key version in its name when working with the keystore?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, It doesn't remove the need for the pin, though. ListCryptoKeys returns CryptoKeys, not versions, so supporting ring listing means something must pick a version to report a public key for. And having GetKeys return the versioned name would break keystore.Reader, which specifies it returns "the keys with the given names". The pin keeps GetKeys and Sign agreeing without changing identities; it's one map under a mutex the public-key cache already needed, and a no-op when version names are configured.

The second cache is unrelated: public keys keyed by version name. Those are immutable, and it saves an RPC per signature.

Happy to drop bare-CryptoKey support entirely if you'd prefer it would delete the pin and most of latestEnabledVersion, at the cost of making every rotation a config change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And having GetKeys return the versioned name would break keystore.Reader, which specifies it returns "the keys with the given names".

So far keystore has been implemented as not-version aware. Unless you would need it in your application, I don't see a reason to introduce this complextity. Hence, I would propose to just use "key/v1" and "key/v2" as fully qualified key names. It will be a client responsibility to choose a correct one.

The second cache is unrelated: public keys keyed by version name. Those are immutable, and it saves an RPC per signature.

Sorry, I don't get it - can't I just call google keystore to sign with "key/v1" and simply fail if this key (or its version) is not available?

func signingKeyNames(listedKeys []*kmspb.CryptoKey) ([]string, error) {
keyNames := make([]string, 0, len(listedKeys))
for _, key := range listedKeys {
if key == nil || key.Name == "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return an error here? Can't you continue in the same way you do it below?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, no strong reason will drop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants