Skip to content

Commit

Permalink
Add support for signing with Tink keyset (#2228)
Browse files Browse the repository at this point in the history
This allows deployers to securely sign in-memory while mitigating key
exfiltration, since the key is encrypted at rest and loaded into memory
at server startup.

Requires providing a path to an encrypted Tink keyset and the Key
Encryption Key, a KMS URI for decrypting the keyset.

Heavily pulls from Fulcio's Tink implementation.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper authored Sep 26, 2024
1 parent 2e4be8b commit 5e341f2
Show file tree
Hide file tree
Showing 10 changed files with 496 additions and 4 deletions.
4 changes: 3 additions & 1 deletion cmd/rekor-server/app/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ func init() {
rootCmd.PersistentFlags().String("rekor_server.address", "127.0.0.1", "Address to bind to")

rootCmd.PersistentFlags().String("rekor_server.signer", "memory",
`Rekor signer to use. Valid options are: [awskms://keyname, azurekms://keyname, gcpkms://keyname, hashivault://keyname, memory, <filename containing PEM-encoded private key>].
`Rekor signer to use. Valid options are: [awskms://keyname, azurekms://keyname, gcpkms://keyname, hashivault://keyname, memory, tink, <filename containing PEM-encoded private key>].
Memory and file-based signers should only be used for testing.`)
rootCmd.PersistentFlags().String("rekor_server.signer-passwd", "", "Password to decrypt signer private key")
rootCmd.PersistentFlags().String("rekor_server.tink_kek_uri", "", "Key encryption key for decrypting Tink keyset. Valid options are [aws-kms://keyname, gcp-kms://keyname]")
rootCmd.PersistentFlags().String("rekor_server.tink_keyset_path", "", "Path to encrypted Tink keyset, containing private key to sign log checkpoints")

rootCmd.PersistentFlags().String("rekor_server.new_entry_publisher", "", "URL for pub/sub queue to send messages to when new entries are added to the log. Ignored if not set. Supported providers: [gcppubsub]")
rootCmd.PersistentFlags().Bool("rekor_server.publish_events_protobuf", false, "Whether to publish events in Protobuf wire format. Applies to all enabled event types.")
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ require (
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.8
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0
github.com/tink-crypto/tink-go/v2 v2.2.0
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c
)
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,12 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI=
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis=
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.2.0 h1:L2Da0F2Udh2agtKztdr69mV/KpnY3/lGTkMgLTVIXlA=
github.com/tink-crypto/tink-go/v2 v2.2.0/go.mod h1:JJ6PomeNPF3cJpfWC0lgyTES6zpJILkAX0cJNwlS3xU=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
Expand Down
5 changes: 4 additions & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ func NewAPI(treeID uint) (*API, error) {
ranges.SetActive(tid)

rekorSigner, err := signer.New(ctx, viper.GetString("rekor_server.signer"),
viper.GetString("rekor_server.signer-passwd"))
viper.GetString("rekor_server.signer-passwd"),
viper.GetString("rekor_server.tink_kek_uri"),
viper.GetString("rekor_server.tink_keyset_path"),
)
if err != nil {
return nil, fmt.Errorf("getting new signer: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/signer/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
func TestMemory(t *testing.T) {
ctx := context.Background()

m, err := New(ctx, "memory", "")
m, err := New(ctx, "memory", "", "", "")
if err != nil {
t.Fatalf("new memory: %v", err)
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/signer/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
_ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault"
)

func New(ctx context.Context, signer string, pass string) (signature.Signer, error) {
func New(ctx context.Context, signer, pass, tinkKEKURI, tinkKeysetPath string) (signature.Signer, error) {
switch {
case slices.ContainsFunc(kms.SupportedProviders(),
func(s string) bool {
Expand All @@ -41,6 +41,8 @@ func New(ctx context.Context, signer string, pass string) (signature.Signer, err
return kms.Get(ctx, signer, crypto.SHA256)
case signer == MemoryScheme:
return NewMemory()
case signer == TinkScheme:
return NewTinkSigner(ctx, tinkKEKURI, tinkKeysetPath)
default:
return NewFile(signer, pass)
}
Expand Down
87 changes: 87 additions & 0 deletions pkg/signer/tink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2024 The Sigstore Authors.
//
// 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 signer

import (
"context"
"errors"
"os"
"path/filepath"
"strings"

tinkUtils "github.com/sigstore/rekor/pkg/signer/tink"
"github.com/tink-crypto/tink-go-awskms/v2/integration/awskms"
"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"

"github.com/sigstore/sigstore/pkg/signature"
)

const TinkScheme = "tink"

// NewTinkSignerWithHandle returns a signature.SignerVerifier that wraps crypto.Signer and a hash function.
// Provide a path to the encrypted keyset and cloud KMS key URI for decryption
func NewTinkSigner(ctx context.Context, kekURI, keysetPath string) (signature.Signer, error) {
kek, err := getKeyEncryptionKey(ctx, kekURI)
if err != nil {
return nil, err
}
return NewTinkSignerWithHandle(kek, keysetPath)
}

// NewTinkSignerWithHandle returns a signature.SignerVerifier that wraps crypto.Signer and a hash function.
// Provide a path to the encrypted keyset and a key handle for decrypting the keyset
func NewTinkSignerWithHandle(kek tink.AEAD, keysetPath string) (signature.Signer, error) {
f, err := os.Open(filepath.Clean(keysetPath))
if err != nil {
return nil, err
}
defer f.Close()

kh, err := keyset.Read(keyset.NewJSONReader(f), kek)
if err != nil {
return nil, err
}
signer, hash, err := tinkUtils.KeyHandleToSigner(kh)
if err != nil {
return nil, err
}
return signature.LoadSignerVerifier(signer, hash)
}

// getKeyEncryptionKey returns a Tink AEAD encryption key from KMS
// Supports GCP and AWS
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)
case strings.HasPrefix(kmsKey, "aws-kms://"):
awsClient, err := awskms.NewClientWithOptions(kmsKey)
if err != nil {
return nil, err
}
registry.RegisterKMSClient(awsClient)
return awsClient.GetAEAD(kmsKey)
default:
return nil, errors.New("unsupported KMS key type")
}
}
157 changes: 157 additions & 0 deletions pkg/signer/tink/tink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2024 The Sigstore Authors.
//
// 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

// Copy of https://github.com/sigstore/fulcio/blob/a781da9903c63a2cd2c0f0c1c2bfc763196db44f/pkg/ca/tinkca/signer.go
// with a modification for including the hash function when creating the signer

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"errors"
"fmt"
"math/big"

"github.com/tink-crypto/tink-go/v2/insecurecleartextkeyset"
"github.com/tink-crypto/tink-go/v2/keyset"
commonpb "github.com/tink-crypto/tink-go/v2/proto/common_go_proto"
ecdsapb "github.com/tink-crypto/tink-go/v2/proto/ecdsa_go_proto"
ed25519pb "github.com/tink-crypto/tink-go/v2/proto/ed25519_go_proto"
tinkpb "github.com/tink-crypto/tink-go/v2/proto/tink_go_proto"
signatureSubtle "github.com/tink-crypto/tink-go/v2/signature/subtle"
"github.com/tink-crypto/tink-go/v2/subtle"
"google.golang.org/protobuf/proto"
)

var (
ecdsaSignerKeyVersion = 0
ecdsaSignerTypeURL = "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey"
ed25519SignerKeyVersion = 0
ed25519SignerTypeURL = "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey"
)

// KeyHandleToSigner converts a key handle to the crypto.Signer interface.
// Heavily pulls from Tink's signature and subtle packages.
func KeyHandleToSigner(kh *keyset.Handle) (crypto.Signer, crypto.Hash, error) {
// extract the key material from the key handle
ks := insecurecleartextkeyset.KeysetMaterial(kh)

k := getPrimaryKey(ks)
if k == nil {
return nil, 0, errors.New("no enabled key found in keyset")
}

switch k.GetTypeUrl() {
case ecdsaSignerTypeURL:
// https://github.com/tink-crypto/tink-go/blob/0aadc94a816408c4bdf95885b3c9860ecfd55fc0/signature/ecdsa/signer_key_manager.go#L48
privKey := new(ecdsapb.EcdsaPrivateKey)
if err := proto.Unmarshal(k.GetValue(), privKey); err != nil {
return nil, 0, fmt.Errorf("error unmarshalling ecdsa private key: %w", err)
}
if err := validateEcdsaPrivKey(privKey); err != nil {
return nil, 0, fmt.Errorf("error validating ecdsa private key: %w", err)
}
// https://github.com/tink-crypto/tink-go/blob/0aadc94a816408c4bdf95885b3c9860ecfd55fc0/signature/subtle/ecdsa_signer.go#L37
hashAlg, curve, _ := getECDSAParamNames(privKey.PublicKey.Params)
p := new(ecdsa.PrivateKey)
c := subtle.GetCurve(curve)
if c == nil {
return nil, 0, errors.New("tink ecdsa signer: invalid curve")
}
p.PublicKey.Curve = c
p.D = new(big.Int).SetBytes(privKey.GetKeyValue())
p.PublicKey.X, p.PublicKey.Y = c.ScalarBaseMult(privKey.GetKeyValue())
hash := getHashFunc(hashAlg)
return p, hash, nil
case ed25519SignerTypeURL:
// https://github.com/tink-crypto/tink-go/blob/0aadc94a816408c4bdf95885b3c9860ecfd55fc0/signature/ed25519/signer_key_manager.go#L47
privKey := new(ed25519pb.Ed25519PrivateKey)
if err := proto.Unmarshal(k.GetValue(), privKey); err != nil {
return nil, 0, fmt.Errorf("error unmarshalling ed25519 private key: %w", err)
}
if err := validateEd25519PrivKey(privKey); err != nil {
return nil, 0, fmt.Errorf("error validating ed25519 private key: %w", err)
}
// https://github.com/tink-crypto/tink-go/blob/0aadc94a816408c4bdf95885b3c9860ecfd55fc0/signature/subtle/ed25519_signer.go#L27
p := ed25519.NewKeyFromSeed(privKey.GetKeyValue())
return p, crypto.SHA512, nil
default:
return nil, 0, fmt.Errorf("unsupported key type: %s", k.GetTypeUrl())
}
}

// getPrimaryKey returns the first enabled key from a keyset.
func getPrimaryKey(ks *tinkpb.Keyset) *tinkpb.KeyData {
for _, k := range ks.GetKey() {
if k.GetKeyId() == ks.GetPrimaryKeyId() && k.GetStatus() == tinkpb.KeyStatusType_ENABLED {
return k.GetKeyData()
}
}
return nil
}

// validateEcdsaPrivKey validates the given ECDSAPrivateKey.
// https://github.com/tink-crypto/tink-go/blob/0aadc94a816408c4bdf95885b3c9860ecfd55fc0/signature/ecdsa/signer_key_manager.go#L151
func validateEcdsaPrivKey(key *ecdsapb.EcdsaPrivateKey) error {
if err := keyset.ValidateKeyVersion(key.Version, uint32(ecdsaSignerKeyVersion)); err != nil {
return fmt.Errorf("ecdsa: invalid key version in key: %s", err)
}
if err := keyset.ValidateKeyVersion(key.GetPublicKey().GetVersion(), uint32(ecdsaSignerKeyVersion)); err != nil {
return fmt.Errorf("ecdsa: invalid public version in key: %s", err)
}
hash, curve, encoding := getECDSAParamNames(key.PublicKey.Params)
return signatureSubtle.ValidateECDSAParams(hash, curve, encoding)
}

// getECDSAParamNames returns the string representations of each parameter in
// the given ECDSAParams.
// https://github.com/tink-crypto/tink-go/blob/0aadc94a816408c4bdf95885b3c9860ecfd55fc0/signature/ecdsa/proto.go#L24
func getECDSAParamNames(params *ecdsapb.EcdsaParams) (string, string, string) {
hashName := commonpb.HashType_name[int32(params.GetHashType())]
curveName := commonpb.EllipticCurveType_name[int32(params.GetCurve())]
encodingName := ecdsapb.EcdsaSignatureEncoding_name[int32(params.GetEncoding())]
return hashName, curveName, encodingName
}

// validateEd25519PrivKey validates the given ED25519PrivateKey.
// https://github.com/tink-crypto/tink-go/blob/0aadc94a816408c4bdf95885b3c9860ecfd55fc0/signature/ed25519/signer_key_manager.go#L157
func validateEd25519PrivKey(key *ed25519pb.Ed25519PrivateKey) error {
if err := keyset.ValidateKeyVersion(key.Version, uint32(ed25519SignerKeyVersion)); err != nil {
return fmt.Errorf("ed25519: invalid key: %w", err)
}
if len(key.KeyValue) != ed25519.SeedSize {
return fmt.Errorf("ed25519: invalid key length, got %d", len(key.KeyValue))
}
return nil
}

// getHashFunc returns the hash function for a given hash name
func getHashFunc(hash string) crypto.Hash {
switch hash {
case "SHA1":
return crypto.SHA1
case "SHA224":
return crypto.SHA224
case "SHA256":
return crypto.SHA256
case "SHA384":
return crypto.SHA384
case "SHA512":
return crypto.SHA512
default:
return crypto.SHA256
}
}
Loading

0 comments on commit 5e341f2

Please sign in to comment.