Add gcpkms keystore implementation - #2338
Conversation
|
👋 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! |
✅ API Diff Results -
|
| // 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. |
There was a problem hiding this comment.
Hm this seems like a breaking change, can we not satisfy this requirement (maybe sort client side)?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.Nameis referenced infake_client.go, causing compilation errors. - Critical (2 votes):
keystore.goreferences the nonexistentAsymmetricSignResponse.Namefield. - Moderate (2 votes): Signers using parent
CryptoKeynames 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
ListCryptoKeyVersionsalso uses the unspecified BASIC view. The resolver requires each listed version'sAlgorithmandCreateTime, 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. RequestCryptoKeyVersionView_FULLhere.
iter := c.client.ListCryptoKeyVersions(ctx, &kmspb.ListCryptoKeyVersionsRequest{Parent: cryptoKeyName})
keystore/gcpkms/client.go:92
ListCryptoKeysis called with the default BASIC/unspecified view. Cloud KMS does not populate the metadata this implementation reads insigningKeyNames(PurposeandVersionTemplate) in that view; in a real key ring those fields remain zero/nil, so asymmetric signing keys can be skipped as non-signing. RequestCryptoKeyView_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 sortingkeyNamesfor both paths instead of weakeningReader'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.
| return &kmspb.AsymmetricSignResponse{ | ||
| Name: req.Name, | ||
| Signature: derSig, | ||
| SignatureCrc32C: wrapperspb.Int64(crc32c(derSig)), | ||
| VerifiedDigestCrc32C: true, |
| if sig.Name != versionName { | ||
| return keystore.SignResponse{}, fmt.Errorf("signing response has name %q, expected %q", sig.Name, versionName) | ||
| } |
| createdAt := resolved.version.CreateTime.AsTime() | ||
| keys = append(keys, keystore.GetKeyResponse{ | ||
| KeyInfo: keystore.NewKeyInfo(keyName, resolved.keyType, createdAt, publicKeyBytes, []byte{}), | ||
| }) |
| // 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. |
There was a problem hiding this comment.
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?
| } | ||
|
|
||
| // 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 |
There was a problem hiding this comment.
Can you link the docs where it mentions this? Would be useful for reviewers (and maybe auditors).
| 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 { |
There was a problem hiding this comment.
nit: can use ed25519.SignatureSize
| // 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 |
There was a problem hiding this comment.
If one could use GOOGLE_APPLICATION_CREDENTIALS for local dev, can we get rid of this credentials file?
| // 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)) |
There was a problem hiding this comment.
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
| // 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 { |
There was a problem hiding this comment.
Do the sub-interfaces need to be defined separately? If so, can we use Client here and name the other one differently?
There was a problem hiding this comment.
Will drop the interface and replace it by an adapter.
| if key.Purpose != kmspb.CryptoKey_ASYMMETRIC_SIGN { | ||
| continue | ||
| } | ||
| if template := key.VersionTemplate; template != nil { |
There was a problem hiding this comment.
what happens if the template is nil? Should the key also be discarded in this case?
There was a problem hiding this comment.
Good catch, VersionTemplate is optional in the API. I will address.
| client Client | ||
| keyRingName string | ||
|
|
||
| mu sync.RWMutex |
There was a problem hiding this comment.
why do you need all these caching mechanism? Can't you include the key version in its name when working with the keystore?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 == "" { |
There was a problem hiding this comment.
Why return an error here? Can't you continue in the same way you do it below?
There was a problem hiding this comment.
Agree, no strong reason will drop.
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.