diff --git a/cmd/tesseract/gcp/main.go b/cmd/tesseract/gcp/main.go index 6dbe821b..6cbe672c 100644 --- a/cmd/tesseract/gcp/main.go +++ b/cmd/tesseract/gcp/main.go @@ -17,6 +17,7 @@ package main import ( "context" + "crypto" "errors" "flag" "fmt" @@ -86,6 +87,8 @@ var ( spannerAntispamDB = flag.String("spanner_antispam_db_path", "", "Spanner antispam deduplication database path projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") 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}.") + 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") traceFraction = flag.Float64("trace_fraction", 0, "Fraction of open-telemetry span traces to sample") otelProjectID = flag.String("otel_project_id", "", "GCP project ID for OpenTelemetry exporter. This is only required for local runs.") ) @@ -99,9 +102,22 @@ func main() { shutdownOTel := initOTel(ctx, *traceFraction, *origin, *otelProjectID) 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/tesseract/gcp/tink.go b/cmd/tesseract/gcp/tink.go new file mode 100644 index 00000000..03a883e3 --- /dev/null +++ b/cmd/tesseract/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/tesseract/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 5aaae255..b6bf9047 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( github.com/google/go-cmp v0.7.0 github.com/kylelemons/godebug v1.1.0 github.com/rivo/tview v0.42.0 + 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/tessera v1.0.0-rc3.0.20250917133736-8b261cbd41ef diff --git a/go.sum b/go.sum index 4fbe7658..346a9939 100644 --- a/go.sum +++ b/go.sum @@ -1023,6 +1023,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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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= 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") + } +}