Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions keystore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k>/cryptoKeyVersions/<n>
// 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
Expand Down Expand Up @@ -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": "<base64-hash>"}'
```
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
Expand Down
5 changes: 4 additions & 1 deletion keystore/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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{
Expand Down
66 changes: 66 additions & 0 deletions keystore/gcpkms/client.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading
Loading