Skip to content
Open
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
22 changes: 19 additions & 3 deletions cmd/tesseract/gcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package main

import (
"context"
"crypto"
"errors"
"flag"
"fmt"
Expand Down Expand Up @@ -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.")
)
Expand All @@ -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{
Expand Down
87 changes: 87 additions & 0 deletions cmd/tesseract/gcp/tink.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
90 changes: 90 additions & 0 deletions internal/tink/tink.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
Loading
Loading