diff --git a/cmd/example-gcp/kms.go b/cmd/example-gcp/kms.go new file mode 100644 index 00000000..d104e9e0 --- /dev/null +++ b/cmd/example-gcp/kms.go @@ -0,0 +1,167 @@ +// Copyright 2024 The Tessera authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "crypto/ed25519" + "crypto/sha256" + "crypto/x509" + "encoding/binary" + "encoding/pem" + "errors" + + kms "cloud.google.com/go/kms/apiv1" + "golang.org/x/mod/sumdb/note" + + "cloud.google.com/go/kms/apiv1/kmspb" +) + +const ( + // KeyVersionNameFormat is the GCP resource identifier for a key version. + // google.cloud.kms.v1.CryptoKeyVersion.name + // https://cloud.google.com/php/docs/reference/cloud-kms/latest/V1.CryptoKeyVersion + KeyVersionNameFormat = "projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/%d" + // From + // https://cs.opensource.google/go/x/mod/+/refs/tags/v0.12.0:sumdb/note/note.go;l=232;drc=baa5c2d058db25484c20d76985ba394e73176132 + algEd25519 = 1 +) + +func publicKeyFromPEM(pemKey []byte) ([]byte, error) { + block, _ := pem.Decode(pemKey) + if block == nil { + return nil, errors.New("failed to decode pemKey") + } + + k, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + publicKey, ok := k.(ed25519.PublicKey) + if !ok { + return nil, errors.New("failed to assert ed25519.PublicKey type") + } + + return publicKey, nil +} + +// keyHash calculates the ed25519 key hash from the key name and public key. +func keyHash(keyName string, publicKey []byte) (uint32, error) { + h := sha256.New() + h.Write([]byte(keyName)) + h.Write([]byte("\n")) + prefixedPublicKey := append([]byte{algEd25519}, publicKey...) + h.Write(prefixedPublicKey) + sum := h.Sum(nil) + + return binary.BigEndian.Uint32(sum), nil +} + +// Signer is an implementation of a +// [note signer](https://pkg.go.dev/golang.org/x/mod/sumdb/note#Signer) which +// interfaces with GCP KMS. +type Signer struct { + // ctx must be stored because Signer is used as an implementation of the + // note.Signer interface, which does not allow for a context in the Sign + // method. However, the KMS AsymmetricSign API requires a context. + ctx context.Context + client *kms.KeyManagementClient + keyHash uint32 + keyName string + kmsKeyName string +} + +// NewKMSSigner creates a signer which uses an Ed25519 key in GCP KMS. +// See https://cloud.google.com/kms/docs/algorithms#elliptic_curve_signing_algorithms +// +// kmsKeyName is the GCP KMS name of the key to be used. +// noteKeyName is the value used as the signer name in the note signature. +func NewKMSSigner(ctx context.Context, c *kms.KeyManagementClient, kmsKeyName, noteKeyName string) (*Signer, error) { + s := &Signer{} + + s.client = c + s.ctx = ctx + s.keyName = noteKeyName + s.kmsKeyName = kmsKeyName + + // Set keyHash. + req := &kmspb.GetPublicKeyRequest{ + Name: kmsKeyName, + } + resp, err := c.GetPublicKey(ctx, req) + if err != nil { + return nil, err + } + + publicKey, err := publicKeyFromPEM([]byte(resp.Pem)) + if err != nil { + return nil, err + } + + kh, err := keyHash(s.keyName, publicKey) + if err != nil { + return nil, err + } + s.keyHash = kh + + return s, nil +} + +// Name identifies the key that this Signer uses. +func (s *Signer) Name() string { + return s.keyName +} + +// KeyHash returns the computed key hash of the signer's public key and name. +// It is used as a hint in identifying the correct key to verify with. +func (s *Signer) KeyHash() uint32 { + return s.keyHash +} + +// Sign returns a signature for the given message. +func (s *Signer) Sign(msg []byte) ([]byte, error) { + req := &kmspb.AsymmetricSignRequest{ + Name: s.kmsKeyName, + Data: msg, + } + resp, err := s.client.AsymmetricSign(s.ctx, req) + if err != nil { + return nil, err + } + + return resp.GetSignature(), nil +} + +// VerifierKeyString returns a string which can be used to create a note +// verifier based on a GCP KMS +// [Ed25519](https://pkg.go.dev/golang.org/x/mod/sumdb/note#hdr-Generating_Keys) +// key. +func VerifierKeyString(ctx context.Context, c *kms.KeyManagementClient, kmsKeyName, noteKeyName string) (string, error) { + req := &kmspb.GetPublicKeyRequest{ + Name: kmsKeyName, + } + resp, err := c.GetPublicKey(ctx, req) + if err != nil { + return "", err + } + + publicKey, err := publicKeyFromPEM([]byte(resp.Pem)) + if err != nil { + return "", err + } + + return note.NewEd25519VerifierKey(noteKeyName, publicKey) +} diff --git a/cmd/example-gcp/main.go b/cmd/example-gcp/main.go index 9d40c846..5413130b 100644 --- a/cmd/example-gcp/main.go +++ b/cmd/example-gcp/main.go @@ -28,6 +28,7 @@ import ( "strings" "time" + kms "cloud.google.com/go/kms/apiv1" tessera "github.com/transparency-dev/trillian-tessera" "github.com/transparency-dev/trillian-tessera/storage/gcp" "golang.org/x/mod/sumdb/note" @@ -35,11 +36,12 @@ import ( ) var ( - bucket = flag.String("bucket", "", "Bucket to use for storing log") - listen = flag.String("listen", ":2024", "Address:port to listen on") - project = flag.String("project", os.Getenv("GOOGLE_CLOUD_PROJECT"), "GCP Project, take from env if unset") - spanner = flag.String("spanner", "", "Spanner resource URI ('projects/.../...')") - signer = flag.String("signer", "", "Path to file containing log private key") + bucket = flag.String("bucket", "", "Bucket to use for storing log") + listen = flag.String("listen", ":2024", "Address:port to listen on") + project = flag.String("project", os.Getenv("GOOGLE_CLOUD_PROJECT"), "GCP Project, take from env if unset") + spanner = flag.String("spanner", "", "Spanner resource URI ('projects/.../...')") + kmsKeyName = flag.String("kms_key", "", "GCP KMS key name for signing checkpoints") + origin = flag.String("origin", "", "Log origin string") ) func main() { @@ -47,13 +49,24 @@ func main() { flag.Parse() ctx := context.Background() + if *origin == "" { + klog.Exit("Must supply --origin") + } + gcpCfg := gcp.Config{ ProjectID: *project, Bucket: *bucket, Spanner: *spanner, } + signer, verifier, kmsClose := signerFromFlags(ctx) + defer func() { + if err := kmsClose(); err != nil { + klog.Errorf("kmsClose(): %v", err) + } + }() + storage, err := gcp.New(ctx, gcpCfg, - tessera.WithCheckpointSignerVerifier(signerFromFlags(), nil), + tessera.WithCheckpointSignerVerifier(signer, verifier), tessera.WithBatching(1024, time.Second), tessera.WithPushback(10*4096), ) @@ -104,14 +117,24 @@ func main() { } } -func signerFromFlags() note.Signer { - raw, err := os.ReadFile(*signer) +// signerFromFlags creates and returns a new KMSSigner from the flags, along with a close func. +func signerFromFlags(ctx context.Context) (note.Signer, note.Verifier, func() error) { + kmClient, err := kms.NewKeyManagementClient(ctx) if err != nil { - klog.Exitf("Failed to read secret key file %q: %v", *signer, err) + klog.Fatalf("Failed to create KeyManagementClient: %v", err) } - signer, err := note.NewSigner(string(raw)) + signer, err := NewKMSSigner(ctx, kmClient, *kmsKeyName, *origin) if err != nil { klog.Exitf("Failed to create new signer: %v", err) } - return signer + vRaw, err := VerifierKeyString(ctx, kmClient, *kmsKeyName, *origin) + if err != nil { + klog.Exitf("Failed to create verifier string: %v", err) + } + verifier, err := note.NewVerifier(vRaw) + if err != nil { + klog.Exitf("Failed to create verifier from %q: %v", vRaw, err) + } + + return signer, verifier, kmClient.Close } diff --git a/go.mod b/go.mod index 9bee108f..029a51b8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/transparency-dev/trillian-tessera go 1.22.5 require ( + cloud.google.com/go/kms v1.18.4 cloud.google.com/go/spanner v1.67.0 cloud.google.com/go/storage v1.43.0 github.com/RobinUS2/golang-moving-average v1.0.0 diff --git a/go.sum b/go.sum index f40b69a2..ea831f41 100644 --- a/go.sum +++ b/go.sum @@ -340,6 +340,8 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4 cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/kms v1.18.4 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk= +cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE=