Skip to content

Commit 07b92d2

Browse files
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>
1 parent 9285389 commit 07b92d2

File tree

6 files changed

+374
-4
lines changed

6 files changed

+374
-4
lines changed

cmd/gcp/main.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package main
1717

1818
import (
1919
"context"
20+
"crypto"
2021
"errors"
2122
"flag"
2223
"fmt"
@@ -62,6 +63,8 @@ var (
6263
signerPublicKeySecretName = flag.String("signer_public_key_secret_name", "", "Public key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.")
6364
signerPrivateKeySecretName = flag.String("signer_private_key_secret_name", "", "Private key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.")
6465
traceFraction = flag.Float64("trace_fraction", 0, "Fraction of open-telemetry span traces to sample")
66+
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}")
67+
signerTinkKeysetFile = flag.String("signer-tink-keyset-path", "", "Path to encrypted Tink keyset")
6568
)
6669

6770
// nolint:staticcheck
@@ -73,9 +76,22 @@ func main() {
7376
shutdownOTel := initOTel(ctx, *traceFraction, *origin)
7477
defer shutdownOTel(ctx)
7578

76-
signer, err := NewSecretManagerSigner(ctx, *signerPublicKeySecretName, *signerPrivateKeySecretName)
77-
if err != nil {
78-
klog.Exitf("Can't create secret manager signer: %v", err)
79+
var signer crypto.Signer
80+
var err error
81+
if *signerPrivateKeySecretName != "" && *signerPublicKeySecretName != "" {
82+
signer, err = NewSecretManagerSigner(ctx, *signerPublicKeySecretName, *signerPrivateKeySecretName)
83+
if err != nil {
84+
klog.Exitf("Can't create secret manager signer: %v", err)
85+
}
86+
}
87+
if *signerTinkKekUri != "" && *signerTinkKeysetFile != "" {
88+
signer, err = NewTinkSignerVerifier(ctx, *signerTinkKekUri, *signerTinkKeysetFile)
89+
if err != nil {
90+
klog.Exitf("Can't initialize Tink signer: %v", err)
91+
}
92+
}
93+
if signer == nil {
94+
klog.Exit("Signer not initialized, provide either a key either in GCP Secret Manager or a GCP KMS-encrypted Tink keyset")
7995
}
8096

8197
chainValidationConfig := tesseract.ChainValidationConfig{

cmd/gcp/tink.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2025 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"crypto"
20+
"crypto/ecdsa"
21+
"crypto/elliptic"
22+
"fmt"
23+
"os"
24+
"path/filepath"
25+
"strings"
26+
27+
"github.com/tink-crypto/tink-go-gcpkms/v2/integration/gcpkms"
28+
"github.com/tink-crypto/tink-go/v2/core/registry"
29+
"github.com/tink-crypto/tink-go/v2/keyset"
30+
"github.com/tink-crypto/tink-go/v2/tink"
31+
tinkUtils "github.com/transparency-dev/static-ct/internal/tink"
32+
)
33+
34+
const TinkScheme = "tink"
35+
36+
// NewTinkSignerVerifier returns a crypto.Signer. Only ECDSA P-256 is supported.
37+
// Provide a path to the encrypted keyset and GCP KMS key URI for decryption.
38+
func NewTinkSignerVerifier(ctx context.Context, kekURI, keysetPath string) (crypto.Signer, error) {
39+
if kekURI == "" || keysetPath == "" {
40+
return nil, fmt.Errorf("key encryption key URI or keyset path unset")
41+
}
42+
kek, err := getKeyEncryptionKey(ctx, kekURI)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
f, err := os.Open(filepath.Clean(keysetPath))
48+
if err != nil {
49+
return nil, err
50+
}
51+
defer f.Close() //nolint: errcheck
52+
53+
kh, err := keyset.Read(keyset.NewJSONReader(f), kek)
54+
if err != nil {
55+
return nil, err
56+
}
57+
signer, err := tinkUtils.KeyHandleToSigner(kh)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
// validate that key is ECDSA P-256
63+
pub, ok := signer.Public().(*ecdsa.PublicKey)
64+
if !ok {
65+
return nil, fmt.Errorf("key must be ECDSA")
66+
}
67+
if pub.Curve != elliptic.P256() {
68+
return nil, fmt.Errorf("elliptic curve must be P-256, was %s", pub.Curve.Params().Name)
69+
}
70+
71+
return signer, err
72+
}
73+
74+
// getKeyEncryptionKey returns a Tink AEAD encryption key from KMS
75+
func getKeyEncryptionKey(ctx context.Context, kmsKey string) (tink.AEAD, error) {
76+
switch {
77+
case strings.HasPrefix(kmsKey, "gcp-kms://"):
78+
gcpClient, err := gcpkms.NewClientWithOptions(ctx, kmsKey)
79+
if err != nil {
80+
return nil, err
81+
}
82+
registry.RegisterKMSClient(gcpClient)
83+
return gcpClient.GetAEAD(kmsKey)
84+
default:
85+
return nil, fmt.Errorf("unsupported KMS key type for key %s", kmsKey)
86+
}
87+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ require (
1919
github.com/google/go-cmp v0.7.0
2020
github.com/kylelemons/godebug v1.1.0
2121
github.com/rivo/tview v0.0.0-20240625185742-b0a7293b8130
22+
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0
23+
github.com/tink-crypto/tink-go/v2 v2.4.0
2224
github.com/transparency-dev/formats v0.0.0-20250414062418-2207ab4ca61e
2325
github.com/transparency-dev/merkle v0.0.2
2426
github.com/transparency-dev/trillian-tessera v0.1.2-0.20250417180933-3f459e774877
@@ -82,6 +84,7 @@ require (
8284
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
8385
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
8486
github.com/mattn/go-runewidth v0.0.16 // indirect
87+
github.com/nxadm/tail v1.4.11 // indirect
8588
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
8689
github.com/rivo/uniseg v0.4.7 // indirect
8790
go.opencensus.io v0.24.0 // indirect

go.sum

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,7 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/
755755
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
756756
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
757757
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
758+
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
758759
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
759760
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
760761
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
@@ -940,8 +941,9 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
940941
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
941942
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
942943
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
943-
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
944944
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
945+
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
946+
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
945947
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
946948
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
947949
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
@@ -1000,6 +1002,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
10001002
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
10011003
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
10021004
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1005+
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0=
1006+
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw=
1007+
github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4dtyHNh0=
1008+
github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw=
10031009
github.com/transparency-dev/formats v0.0.0-20250414062418-2207ab4ca61e h1:FdaLjgEMBy6I/7aVpkzlvLOeNDwKzkAzvNPAoSbGBtM=
10041010
github.com/transparency-dev/formats v0.0.0-20250414062418-2207ab4ca61e/go.mod h1:ODywn0gGarHMMdSkWT56ULoK8Hk71luOyRseKek9COw=
10051011
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
@@ -1323,6 +1329,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
13231329
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13241330
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13251331
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1332+
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13261333
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13271334
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13281335
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/tink/tink.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package tink
16+
17+
import (
18+
"crypto"
19+
"crypto/ecdsa"
20+
"crypto/ed25519"
21+
"crypto/elliptic"
22+
"fmt"
23+
"math/big"
24+
25+
"github.com/tink-crypto/tink-go/v2/insecuresecretdataaccess"
26+
"github.com/tink-crypto/tink-go/v2/keyset"
27+
tinkecdsa "github.com/tink-crypto/tink-go/v2/signature/ecdsa"
28+
tinked25519 "github.com/tink-crypto/tink-go/v2/signature/ed25519"
29+
)
30+
31+
func curveFromTinkECDSACurveType(curveType tinkecdsa.CurveType) (elliptic.Curve, error) {
32+
switch curveType {
33+
case tinkecdsa.NistP256:
34+
return elliptic.P256(), nil
35+
case tinkecdsa.NistP384:
36+
return elliptic.P384(), nil
37+
case tinkecdsa.NistP521:
38+
return elliptic.P521(), nil
39+
default:
40+
// Should never happen.
41+
return nil, fmt.Errorf("unsupported curve: %v", curveType)
42+
}
43+
}
44+
45+
// KeyHandleToSigner constructs a [crypto.Signer] from a Tink [keyset.Handle]'s
46+
// primary key.
47+
//
48+
// NOTE: Tink validates keys on [keyset.Handle] creation.
49+
func KeyHandleToSigner(kh *keyset.Handle) (crypto.Signer, error) {
50+
primary, err := kh.Primary()
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
switch privateKey := primary.Key().(type) {
56+
case *tinkecdsa.PrivateKey:
57+
publicKey, err := privateKey.PublicKey()
58+
if err != nil {
59+
return nil, err
60+
}
61+
ecdsaPublicKey, ok := publicKey.(*tinkecdsa.PublicKey)
62+
if !ok {
63+
return nil, fmt.Errorf("error asserting ecdsa public key")
64+
}
65+
66+
curveParams, ok := ecdsaPublicKey.Parameters().(*tinkecdsa.Parameters)
67+
if !ok {
68+
return nil, fmt.Errorf("error asserting ecdsa parameters")
69+
}
70+
curve, err := curveFromTinkECDSACurveType(curveParams.CurveType())
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
// Encoded as: 0x04 || X || Y.
76+
// See https://github.com/tink-crypto/tink-go/blob/v2.3.0/signature/ecdsa/key.go#L335
77+
publicPoint := ecdsaPublicKey.PublicPoint()
78+
xy := publicPoint[1:]
79+
pk := new(ecdsa.PrivateKey)
80+
pk.Curve = curve
81+
pk.X = new(big.Int).SetBytes(xy[:len(xy)/2])
82+
pk.Y = new(big.Int).SetBytes(xy[len(xy)/2:])
83+
pk.D = new(big.Int).SetBytes(privateKey.PrivateKeyValue().Data(insecuresecretdataaccess.Token{}))
84+
return pk, err
85+
case *tinked25519.PrivateKey:
86+
return ed25519.NewKeyFromSeed(privateKey.PrivateKeyBytes().Data(insecuresecretdataaccess.Token{})), err
87+
default:
88+
return nil, fmt.Errorf("unsupported key type: %T", primary.Key())
89+
}
90+
}

0 commit comments

Comments
 (0)