Skip to content

Add support for Tink keyset signer #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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/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 @@ -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
Expand All @@ -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{
Expand Down
87 changes: 87 additions & 0 deletions cmd/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/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)
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
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