Skip to content
Merged
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
31 changes: 31 additions & 0 deletions .github/workflows/create-tink-keyset-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Create Tink Keyset Test

on:
pull_request:
branches:
- main
paths:
- 'cmd/create-tink-keyset/**'

permissions:
contents: read

jobs:
create-tink-keyset-test:
name: 'Build Create Tink Keyset'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: 'Checkout'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: 'go.mod'
check-latest: true

- name: Create Tink Keyset build
id: create-tink-keyset-test
run: go build ./cmd/create-tink-keyset
43 changes: 43 additions & 0 deletions cmd/create-tink-keyset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# `create-tink-keyset`

This command generates a Tink keyset that can be used for signing. The generated keyset is encrypted with a Key Encryption Key (KEK) from a cloud KMS. Currently, only Google Cloud KMS is supported.

The command also outputs the corresponding public key in PEM format. This is useful for TUF, where the public key needs to be distributed for clients.

## Prerequisites

You must have a Key Encryption Key (KEK) available in Google Cloud KMS. You also need to have credentials configured locally to be able to access the KMS key.
You can initialize credentials with `gcloud auth application-default login`.

## Usage

```shell
go run ./cmd/create-tink-keyset [flags]
```

### Flags

* `--key-template` **(required)**: The Tink key template for the signing algorithm.
* Valid values: `ED25519`, `ECDSA_P256`, `ECDSA_P384_SHA384`, `ECDSA_P521`.
* `--key-encryption-key-uri` **(required)**: The resource URI for the KMS key that will encrypt the keyset.
* Only GCP is supported.
* The URI must be in the format `gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*`.
* `--out` **(required)**: The output path for the encrypted Tink keyset file.
* `--public-key-out` **(required)**: The output path for the PEM-encoded public key.

## Example

```shell
go run ./cmd/create-tink-keyset \
--key-template ED25519 \
--out enc-keyset.cfg \
--key-encryption-key-uri gcp-kms://projects/my-gcp-project/locations/global/keyRings/my-keyring/cryptoKeys/my-kek \
--public-key-out public.pem
```

## Outputs

The command generates two files:

1. **`--out` file (e.g., `enc-keyset.cfg`)**: An encrypted Tink keyset in JSON format. This file contains the private key, encrypted by the specified GCP KMS key. This file should be kept private.
2. **`--public-key-out` file (e.g., `public.pem`)**: The corresponding public key in PEM format, to verify signatures created with the private key from the keyset.
198 changes: 198 additions & 0 deletions cmd/create-tink-keyset/app/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright 2025 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 app

import (
"bytes"
"context"
"crypto/ecdh"
"crypto/ed25519"
"log/slog"
"os"
"strings"

"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tink-crypto/tink-go-gcpkms/v2/integration/gcpkms"
"github.com/tink-crypto/tink-go/v2/keyset"
"github.com/tink-crypto/tink-go/v2/proto/tink_go_proto"
"github.com/tink-crypto/tink-go/v2/signature"
tinkecdsa "github.com/tink-crypto/tink-go/v2/signature/ecdsa"
tinked25519 "github.com/tink-crypto/tink-go/v2/signature/ed25519"
)

/*
Example command:

go run ./cmd/create-tink-keyset \
--key-template ED25519 \
--out enc-keyset.cfg \
--key-encryption-key-uri gcp-kms://projects/project/locations/us-west1/keyRings/keyring/cryptoKeys/keyname \
--public-key-out key.pem
*/

var rootCmd = &cobra.Command{
Use: "create-tink-keyset",
Short: "Create a Tink keyset",
Long: "Generate a Tink keyset to be used to sign checkpoints, encrypted with a provided KMS key. Only supported for GCP currently.",
Run: func(_ *cobra.Command, _ []string) {
if viper.GetString("key-template") == "" {
slog.Error("must provide --key-template for signing key algorithm")
os.Exit(1)
}
kekURI := viper.GetString("key-encryption-key-uri")
if kekURI == "" {
slog.Error("must provide --key-encryption-key-uri for the GCP KMS CryptoKey resource that encrypts the keyset")
os.Exit(1)
}
if !strings.HasPrefix(kekURI, "gcp-kms://") {
slog.Error("--key-encryption-key-uri only supports GCP and the URI must begin with gcp-kms://")
os.Exit(1)
}
if viper.GetString("out") == "" {
slog.Error("must provide --out for output path of keyset")
os.Exit(1)
}
if viper.GetString("public-key-out") == "" {
slog.Error("must provide --public-key-out for output path of public key")
os.Exit(1)
}

ctx := context.Background()

// Generate GCP KMS client
kmsClient, err := gcpkms.NewClientWithOptions(ctx, kekURI)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
kekAEAD, err := kmsClient.GetAEAD(kekURI)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}

// Create keyset handle, which initializes the signing key based on the provided template
keyTemplate, ok := algToKeyTemplate[viper.GetString("key-template")]
if !ok {
slog.Error("unsupported key template provided")
os.Exit(1)
}
newHandle, err := keyset.NewHandle(keyTemplate)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}

// Encrypt signing key and generate keyset
buf := new(bytes.Buffer)
writer := keyset.NewJSONWriter(buf)
if err := newHandle.Write(writer, kekAEAD); err != nil {
slog.Error(err.Error())
os.Exit(1)
}

f, err := os.Create(viper.GetString("out"))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
defer f.Close()
_, err = f.Write(buf.Bytes())
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}

// Generate PEM-encoded public key
publicHandle, err := newHandle.Public()
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
keyEntry, err := publicHandle.Primary()
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
var pemPubKey []byte
switch publicKey := keyEntry.Key().(type) {
case *tinked25519.PublicKey:
pemPubKey, err = cryptoutils.MarshalPublicKeyToPEM(ed25519.PublicKey(publicKey.KeyBytes()))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
case *tinkecdsa.PublicKey:
curve := algToCurve[viper.GetString("key-template")]
pubKey, err := curve.NewPublicKey(publicKey.PublicPoint())
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
pemPubKey, err = cryptoutils.MarshalPublicKeyToPEM(pubKey)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}
pubF, err := os.Create(viper.GetString("public-key-out"))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
defer pubF.Close()
_, err = pubF.Write(pemPubKey)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}

slog.Info("generated Tink keyset")
},
}

var algToKeyTemplate = map[string]*tink_go_proto.KeyTemplate{
"ED25519": signature.ED25519KeyTemplate(),
"ECDSA_P256": signature.ECDSAP256KeyTemplate(),
"ECDSA_P384_SHA384": signature.ECDSAP384SHA384KeyTemplate(),
"ECDSA_P521": signature.ECDSAP521KeyTemplate(),
}

var algToCurve = map[string]ecdh.Curve{
"ECDSA_P256": ecdh.P256(),
"ECDSA_P384_SHA384": ecdh.P384(),
"ECDSA_P521": ecdh.P521(),
}

func Execute() {
if err := rootCmd.Execute(); err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}

func init() {
rootCmd.Flags().String("key-template", "", "tink key template for the signing algorithm. Valid values are ED25519, ECDSA_P256, ECDSA_P384_SHA384, and ECDSA_P521")
rootCmd.Flags().String("key-encryption-key-uri", "", "Resource URI for the KMS key that encrypts the keyset. Only GCP is supported, and the URI must match gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*")
rootCmd.Flags().String("out", "", "output path for the encrypted keyset")
rootCmd.Flags().String("public-key-out", "", "output path for the PEM-encoded public key")

if err := viper.BindPFlags(rootCmd.Flags()); err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}
21 changes: 21 additions & 0 deletions cmd/create-tink-keyset/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2025 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 main

import "github.com/sigstore/scaffolding/cmd/create-tink-keyset/app"

func main() {
app.Execute()
}
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ require (
github.com/sigstore/sigstore v1.9.5
github.com/sigstore/sigstore-go v1.0.0
github.com/sigstore/timestamp-authority v1.2.8
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/theupdateframework/go-tuf v0.7.0
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0
github.com/tink-crypto/tink-go/v2 v2.4.0
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399
go.step.sm/crypto v0.67.0
go.uber.org/zap v1.27.0
Expand Down Expand Up @@ -249,18 +253,14 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/theupdateframework/go-tuf/v2 v2.1.1 // indirect
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 // indirect
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 // indirect
github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0 // indirect
github.com/tink-crypto/tink-go/v2 v2.4.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
Expand Down
Loading