From b6ba9ff718f999ea40ffcf2be5b9779e690d7186 Mon Sep 17 00:00:00 2001 From: Hayden B <8418760+haydentherapper@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:08:36 -0700 Subject: [PATCH] Add support for Tink keyset signer This PR adds support for in-memory signing using a Tink keyset. The keyset is encrypted with a key-encryption-key stored in GCP KMS. The key is decrypted on startup and loaded into memory. This uses a utility to unpack the keyset into a crypto.Signer so that it can be used to sign certificates. This also validates that the key is an ECDSA P-256 key as per RFC 6962, since Tink supports many key types. Signed-off-by: Hayden B <8418760+haydentherapper@users.noreply.github.com> --- cmd/gcp/main.go | 22 ++++- cmd/gcp/tink.go | 87 +++++++++++++++++++ go.mod | 3 + go.sum | 9 +- internal/tink/tink.go | 90 ++++++++++++++++++++ internal/tink/tink_test.go | 167 +++++++++++++++++++++++++++++++++++++ 6 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 cmd/gcp/tink.go create mode 100644 internal/tink/tink.go create mode 100644 internal/tink/tink_test.go diff --git a/cmd/gcp/main.go b/cmd/gcp/main.go index 8dc1c674..856caccd 100644 --- a/cmd/gcp/main.go +++ b/cmd/gcp/main.go @@ -17,6 +17,7 @@ package main import ( "context" + "crypto" "errors" "flag" "fmt" @@ -62,6 +63,8 @@ var ( signerPublicKeySecretName = flag.String("signer_public_key_secret_name", "", "Public key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.") signerPrivateKeySecretName = flag.String("signer_private_key_secret_name", "", "Private key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.") traceFraction = flag.Float64("trace_fraction", 0, "Fraction of open-telemetry span traces to sample") + signerTinkKekUri = flag.String("signer-tink-kek-uri", "", "Encryption key for decrypting Tink keyset. Format: gcp-kms://projects/{projectId}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{cryptoKey}/cryptoKeyVersions/{version}") + signerTinkKeysetFile = flag.String("signer-tink-keyset-path", "", "Path to encrypted Tink keyset") ) // nolint:staticcheck @@ -73,9 +76,22 @@ func main() { shutdownOTel := initOTel(ctx, *traceFraction, *origin) defer shutdownOTel(ctx) - signer, err := NewSecretManagerSigner(ctx, *signerPublicKeySecretName, *signerPrivateKeySecretName) - if err != nil { - klog.Exitf("Can't create secret manager signer: %v", err) + var signer crypto.Signer + var err error + if *signerPrivateKeySecretName != "" && *signerPublicKeySecretName != "" { + signer, err = NewSecretManagerSigner(ctx, *signerPublicKeySecretName, *signerPrivateKeySecretName) + if err != nil { + klog.Exitf("Can't create secret manager signer: %v", err) + } + } + if *signerTinkKekUri != "" && *signerTinkKeysetFile != "" { + signer, err = NewTinkSignerVerifier(ctx, *signerTinkKekUri, *signerTinkKeysetFile) + if err != nil { + klog.Exitf("Can't initialize Tink signer: %v", err) + } + } + if signer == nil { + klog.Exit("Signer not initialized, provide either a key either in GCP Secret Manager or a GCP KMS-encrypted Tink keyset") } chainValidationConfig := tesseract.ChainValidationConfig{ diff --git a/cmd/gcp/tink.go b/cmd/gcp/tink.go new file mode 100644 index 00000000..697a2394 --- /dev/null +++ b/cmd/gcp/tink.go @@ -0,0 +1,87 @@ +// Copyright 2025 Google LLC. 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" + "crypto/ecdsa" + "crypto/elliptic" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/tink-crypto/tink-go-gcpkms/v2/integration/gcpkms" + "github.com/tink-crypto/tink-go/v2/core/registry" + "github.com/tink-crypto/tink-go/v2/keyset" + "github.com/tink-crypto/tink-go/v2/tink" + tinkUtils "github.com/transparency-dev/static-ct/internal/tink" +) + +const TinkScheme = "tink" + +// NewTinkSignerVerifier returns a crypto.Signer. Only ECDSA P-256 is supported. +// Provide a path to the encrypted keyset and GCP KMS key URI for decryption. +func NewTinkSignerVerifier(ctx context.Context, kekURI, keysetPath string) (crypto.Signer, error) { + if kekURI == "" || keysetPath == "" { + return nil, fmt.Errorf("key encryption key URI or keyset path unset") + } + kek, err := getKeyEncryptionKey(ctx, kekURI) + if err != nil { + return nil, err + } + + f, err := os.Open(filepath.Clean(keysetPath)) + if err != nil { + return nil, err + } + defer f.Close() //nolint: errcheck + + kh, err := keyset.Read(keyset.NewJSONReader(f), kek) + if err != nil { + return nil, err + } + signer, err := tinkUtils.KeyHandleToSigner(kh) + if err != nil { + return nil, err + } + + // validate that key is ECDSA P-256 + pub, ok := signer.Public().(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("key must be ECDSA") + } + if pub.Curve != elliptic.P256() { + return nil, fmt.Errorf("elliptic curve must be P-256, was %s", pub.Curve.Params().Name) + } + + return signer, err +} + +// getKeyEncryptionKey returns a Tink AEAD encryption key from KMS +func getKeyEncryptionKey(ctx context.Context, kmsKey string) (tink.AEAD, error) { + switch { + case strings.HasPrefix(kmsKey, "gcp-kms://"): + gcpClient, err := gcpkms.NewClientWithOptions(ctx, kmsKey) + if err != nil { + return nil, err + } + registry.RegisterKMSClient(gcpClient) + return gcpClient.GetAEAD(kmsKey) + default: + return nil, fmt.Errorf("unsupported KMS key type for key %s", kmsKey) + } +} diff --git a/go.mod b/go.mod index 08a5de5f..36f4c39b 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,8 @@ require ( github.com/google/go-cmp v0.7.0 github.com/kylelemons/godebug v1.1.0 github.com/rivo/tview v0.0.0-20240625185742-b0a7293b8130 + github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 + github.com/tink-crypto/tink-go/v2 v2.4.0 github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 github.com/transparency-dev/merkle v0.0.2 github.com/transparency-dev/trillian-tessera v0.1.2 @@ -83,6 +85,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index 790527ed..0fb00d4f 100644 --- a/go.sum +++ b/go.sum @@ -755,6 +755,7 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= @@ -942,8 +943,9 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= @@ -1004,6 +1006,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= +github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4dtyHNh0= +github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw= github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 h1:YTbkeFbzcer+42bIgo6Za2194nKwhZPgaZKsP76QffE= github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26/go.mod h1:ODywn0gGarHMMdSkWT56ULoK8Hk71luOyRseKek9COw= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= @@ -1329,6 +1335,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/tink/tink.go b/internal/tink/tink.go new file mode 100644 index 00000000..e940d103 --- /dev/null +++ b/internal/tink/tink.go @@ -0,0 +1,90 @@ +// Copyright 2025 Google LLC. 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 tink + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "fmt" + "math/big" + + "github.com/tink-crypto/tink-go/v2/insecuresecretdataaccess" + "github.com/tink-crypto/tink-go/v2/keyset" + tinkecdsa "github.com/tink-crypto/tink-go/v2/signature/ecdsa" + tinked25519 "github.com/tink-crypto/tink-go/v2/signature/ed25519" +) + +func curveFromTinkECDSACurveType(curveType tinkecdsa.CurveType) (elliptic.Curve, error) { + switch curveType { + case tinkecdsa.NistP256: + return elliptic.P256(), nil + case tinkecdsa.NistP384: + return elliptic.P384(), nil + case tinkecdsa.NistP521: + return elliptic.P521(), nil + default: + // Should never happen. + return nil, fmt.Errorf("unsupported curve: %v", curveType) + } +} + +// KeyHandleToSigner constructs a [crypto.Signer] from a Tink [keyset.Handle]'s +// primary key. +// +// NOTE: Tink validates keys on [keyset.Handle] creation. +func KeyHandleToSigner(kh *keyset.Handle) (crypto.Signer, error) { + primary, err := kh.Primary() + if err != nil { + return nil, err + } + + switch privateKey := primary.Key().(type) { + case *tinkecdsa.PrivateKey: + publicKey, err := privateKey.PublicKey() + if err != nil { + return nil, err + } + ecdsaPublicKey, ok := publicKey.(*tinkecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("error asserting ecdsa public key") + } + + curveParams, ok := ecdsaPublicKey.Parameters().(*tinkecdsa.Parameters) + if !ok { + return nil, fmt.Errorf("error asserting ecdsa parameters") + } + curve, err := curveFromTinkECDSACurveType(curveParams.CurveType()) + if err != nil { + return nil, err + } + + // Encoded as: 0x04 || X || Y. + // See https://github.com/tink-crypto/tink-go/blob/v2.3.0/signature/ecdsa/key.go#L335 + publicPoint := ecdsaPublicKey.PublicPoint() + xy := publicPoint[1:] + pk := new(ecdsa.PrivateKey) + pk.Curve = curve + pk.X = new(big.Int).SetBytes(xy[:len(xy)/2]) + pk.Y = new(big.Int).SetBytes(xy[len(xy)/2:]) + pk.D = new(big.Int).SetBytes(privateKey.PrivateKeyValue().Data(insecuresecretdataaccess.Token{})) + return pk, err + case *tinked25519.PrivateKey: + return ed25519.NewKeyFromSeed(privateKey.PrivateKeyBytes().Data(insecuresecretdataaccess.Token{})), err + default: + return nil, fmt.Errorf("unsupported key type: %T", primary.Key()) + } +} diff --git a/internal/tink/tink_test.go b/internal/tink/tink_test.go new file mode 100644 index 00000000..84cc763a --- /dev/null +++ b/internal/tink/tink_test.go @@ -0,0 +1,167 @@ +// Copyright 2025 Google LLC. 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 tink + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "hash" + "testing" + + "github.com/tink-crypto/tink-go/v2/keyset" + tinkpb "github.com/tink-crypto/tink-go/v2/proto/tink_go_proto" + "github.com/tink-crypto/tink-go/v2/signature" +) + +func TestKeyHandleToSignerECDSA(t *testing.T) { + for _, tc := range []struct { + name string + keyTemplate *tinkpb.KeyTemplate + h hash.Hash + }{ + { + name: "ECDSA-P256-SHA256", + keyTemplate: signature.ECDSAP256KeyWithoutPrefixTemplate(), + h: sha256.New(), + }, + { + name: "ECDSA-P384-SHA512", + keyTemplate: signature.ECDSAP384KeyWithoutPrefixTemplate(), + h: sha512.New(), + }, + { + name: "ECDSA-P521-SHA512", + keyTemplate: signature.ECDSAP521KeyWithoutPrefixTemplate(), + h: sha512.New(), + }, + } { + t.Run(tc.name, func(t *testing.T) { + kh, err := keyset.NewHandle(tc.keyTemplate) + if err != nil { + t.Fatalf("error creating ECDSA key handle: %v", err) + } + // convert to crypto.Signer interface + signer, err := KeyHandleToSigner(kh) + if err != nil { + t.Fatalf("error converting ECDSA key handle to signer: %v", err) + } + msg := []byte("hello there") + + // sign with key handle, verify with signer public key + tinkSigner, err := signature.NewSigner(kh) + if err != nil { + t.Fatalf("error creating tink signer: %v", err) + } + sig, err := tinkSigner.Sign(msg) + if err != nil { + t.Fatalf("error signing with tink signer: %v", err) + } + tc.h.Write(msg) + digest := tc.h.Sum(nil) + publicKey, ok := signer.Public().(*ecdsa.PublicKey) + if !ok { + t.Fatalf("error asserting ecdsa public key") + } + if !ecdsa.VerifyASN1(publicKey, digest, sig) { + t.Fatalf("signature from tink signer did not match") + } + + // sign with signer, verify with key handle + privKey, ok := signer.(*ecdsa.PrivateKey) + if !ok { + t.Fatalf("error asserting ecdsa private key") + } + sig, err = ecdsa.SignASN1(rand.Reader, privKey, digest) + if err != nil { + t.Fatalf("error signing with crypto signer: %v", err) + } + pubkh, err := kh.Public() + if err != nil { + t.Fatalf("error fetching public key handle: %v", err) + } + v, err := signature.NewVerifier(pubkh) + if err != nil { + t.Fatalf("error creating tink verifier: %v", err) + } + if err := v.Verify(sig, msg); err != nil { + t.Fatalf("error verifying with tink verifier: %v", err) + } + }) + } +} + +func TestKeyHandleToSignerED25519(t *testing.T) { + kh, err := keyset.NewHandle(signature.ED25519KeyWithoutPrefixTemplate()) + if err != nil { + t.Fatalf("error creating ED25519 key handle: %v", err) + } + // convert to crypto.Signer interface + signer, err := KeyHandleToSigner(kh) + if err != nil { + t.Fatalf("error converting ED25519 key handle to signer: %v", err) + } + msg := []byte("hello there") + + // sign with key handle, verify with signer public key + tinkSigner, err := signature.NewSigner(kh) + if err != nil { + t.Fatalf("error creating tink signer: %v", err) + } + sig, err := tinkSigner.Sign(msg) + if err != nil { + t.Fatalf("error signing with tink signer: %v", err) + } + publicKey, ok := signer.Public().(ed25519.PublicKey) + if !ok { + t.Fatalf("error asserting ed25519 public key") + } + if !ed25519.Verify(publicKey, msg, sig) { + t.Fatalf("signature from tink signer did not match") + } + + // sign with signer, verify with key handle + privKey, ok := signer.(ed25519.PrivateKey) + if !ok { + t.Fatalf("error asserting ed25519 private key") + } + sig = ed25519.Sign(privKey, msg) + if err != nil { + t.Fatalf("error signing with crypto signer: %v", err) + } + pubkh, err := kh.Public() + if err != nil { + t.Fatalf("error fetching public key handle: %v", err) + } + v, err := signature.NewVerifier(pubkh) + if err != nil { + t.Fatalf("error creating tink verifier: %v", err) + } + if err := v.Verify(sig, msg); err != nil { + t.Fatalf("error verifying with tink verifier: %v", err) + } +} + +func TestKeyHandleToSignerFailsWithInvalidKeyType(t *testing.T) { + kh, err := keyset.NewHandle(signature.RSA_SSA_PKCS1_3072_SHA256_F4_RAW_Key_Template()) + if err != nil { + t.Fatalf("keyset.NewHandle() err = %v, want nil", err) + } + if _, err := KeyHandleToSigner(kh); err == nil { + t.Errorf("KeyHandleToSigner(kh) err = nil, want error") + } +}