Skip to content

Commit

Permalink
Add Tink signing backend
Browse files Browse the repository at this point in the history
This adds support for using encrypted Tink keysets to load a signer.
There are two main benefits from this work: We can leverage this instead
of KMS if we need to support a higher QPS, and Tink keysets use strong
secure defaults. Keysets can be encrypted with AESGCM, do not rely on a
KDF, cannot be brute-forced, and access to the key can be audited
through cloud audit logs.

Tink does not provide a method to extract the signing key from the
keyset intentionally, so I wrote a helper library to reach into the key
handle proto to construct a crypto.Signer.

Signed-off-by: Hayden Blauzvern <[email protected]>
  • Loading branch information
haydentherapper committed Jun 20, 2022
1 parent d54330c commit 3fda04f
Show file tree
Hide file tree
Showing 12 changed files with 739 additions and 16 deletions.
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ issues:
linters:
- staticcheck
text: SA1019
- path: pkg/ca/tinkca/signer.go
linters:
- staticcheck
text: SA1019
max-issues-per-linter: 0
max-same-issues: 0
run:
Expand Down
19 changes: 18 additions & 1 deletion cmd/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
googlecav1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1"
"github.com/sigstore/fulcio/pkg/ca/kmsca"
"github.com/sigstore/fulcio/pkg/ca/pkcs11ca"
"github.com/sigstore/fulcio/pkg/ca/tinkca"
"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/log"
"github.com/spf13/cobra"
Expand All @@ -57,7 +58,7 @@ func newServeCmd() *cobra.Command {

cmd.Flags().StringVarP(&serveCmdConfigFilePath, "config", "c", "", "config file containing all settings")
cmd.Flags().String("log_type", "dev", "logger type to use (dev/prod)")
cmd.Flags().String("ca", "", "googleca | pkcs11ca | fileca | kmsca | ephemeralca (for testing)")
cmd.Flags().String("ca", "", "googleca | tinkca | pkcs11ca | fileca | kmsca | ephemeralca (for testing)")
cmd.Flags().String("aws-hsm-root-ca-path", "", "Path to root CA on disk (only used with AWS HSM)")
cmd.Flags().String("gcp_private_ca_parent", "", "private ca parent: /projects/<project>/locations/<location>/<name> (only used with --ca googleca)")
cmd.Flags().String("hsm-caroot-id", "", "HSM ID for Root CA (only used with --ca pkcs11ca)")
Expand All @@ -71,6 +72,9 @@ func newServeCmd() *cobra.Command {
cmd.Flags().Bool("fileca-watch", true, "Watch filesystem for updates")
cmd.Flags().String("kms-resource", "", "KMS key resource path. Must be prefixed with awskms://, azurekms://, gcpkms://, or hashivault://")
cmd.Flags().String("kms-cert-chain-path", "", "Path to PEM-encoded CA certificate chain for KMS-backed CA")
cmd.Flags().String("tink-kms-resource", "", "KMS key resource path for encrypted Tink keyset. Must be prefixed with gcp-kms:// or aws-kms://")
cmd.Flags().String("tink-cert-chain-path", "", "Path to PEM-encoded CA certificate chain for Tink-backed CA")
cmd.Flags().String("tink-keyset-path", "", "Path to KMS-encrypted keyset for Tink-backed CA")
cmd.Flags().String("host", "0.0.0.0", "The host on which to serve requests for HTTP; --http-host is alias")
cmd.Flags().String("port", "8080", "The port on which to serve requests for HTTP; --http-port is alias")
cmd.Flags().String("grpc-host", "0.0.0.0", "The host on which to serve requests for GRPC")
Expand Down Expand Up @@ -154,6 +158,16 @@ func runServeCmd(cmd *cobra.Command, args []string) {
if !viper.IsSet("kms-cert-chain-path") {
log.Logger.Fatal("kms-cert-chain-path must be set when using kmsca")
}
case "tinkca":
if !viper.IsSet("tink-kms-resource") {
log.Logger.Fatal("tink-kms-resource must be set when using tinkca")
}
if !viper.IsSet("tink-cert-chain-path") {
log.Logger.Fatal("tink-cert-chain-path must be set when using tinkca")
}
if !viper.IsSet("tink-keyset-path") {
log.Logger.Fatal("tink-keyset-path must be set when using tinkca")
}
case "ephemeralca":
// this is a no-op since this is a self-signed in-memory CA for testing
default:
Expand Down Expand Up @@ -196,6 +210,9 @@ func runServeCmd(cmd *cobra.Command, args []string) {
baseca, err = ephemeralca.NewEphemeralCA()
case "kmsca":
baseca, err = kmsca.NewKMSCA(cmd.Context(), viper.GetString("kms-resource"), viper.GetString("kms-cert-chain-path"))
case "tinkca":
baseca, err = tinkca.NewTinkCA(cmd.Context(),
viper.GetString("tink-kms-resource"), viper.GetString("tink-keyset-path"), viper.GetString("tink-cert-chain-path"))
default:
err = fmt.Errorf("invalid value for configured CA: %v", baseca)
}
Expand Down
73 changes: 73 additions & 0 deletions cmd/create_tink_keyset/create_tink_keyset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2022 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 (
"flag"
"fmt"
"log"
"os"

"github.com/google/tink/go/keyset"
"github.com/google/tink/go/signature"
"github.com/sigstore/fulcio/pkg/ca/tinkca"
)

/*
To run:
go run cmd/create_tink_keyset/create_tink_keyset.go \
--kms-resource="gcp-kms://projects/<project>/locations/<region>/keyRings/<key-ring>/cryptoKeys/<key>" \
--output="enc-keyset.cfg"
You can also create a GCP KMS encrypted Tink keyset with tinkey:
tinkey create-keyset --key-template ECDSA_P384 --out enc-keyset.cfg --master-key-uri gcp-kms://projects/<project>/locations/<region>/keyRings/<key-ring>/cryptoKeys/<key>
You must have the permissions to read the KMS key, and create a certificate in the CA pool.
*/

var (
kmsKey = flag.String("kms-resource", "", "Resource path to KMS key, starting with gcp-kms:// or aws-kms://")
outputPath = flag.String("output", "", "Path to the output file")
)

func main() {
flag.Parse()
if *kmsKey == "" {
log.Fatal("kms-resource must be set")
}
if *outputPath == "" {
log.Fatal("output must be set")
}

kh, err := keyset.NewHandle(signature.ECDSAP384KeyWithoutPrefixTemplate())
if err != nil {
log.Fatal(err)
}

primaryKey, err := tinkca.GetPrimaryKey(*kmsKey)
if err != nil {
log.Fatal(err)
}

f, err := os.Create(*outputPath)
if err != nil {
log.Fatal(err)
}
defer f.Close()
jsonWriter := keyset.NewJSONWriter(f)
if err := kh.Write(jsonWriter, primaryKey); err != nil {
fmt.Printf("error writing primary key: %v\n", err)
}
}
65 changes: 51 additions & 14 deletions cmd/fetch_ca_cert/fetch_ca_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ import (
"flag"
"log"
"os"
"path/filepath"
"time"

privateca "cloud.google.com/go/security/privateca/apiv1"
"github.com/google/tink/go/keyset"
"github.com/sigstore/fulcio/pkg/ca/tinkca"
"github.com/sigstore/sigstore/pkg/cryptoutils"
privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1"
"google.golang.org/protobuf/types/known/durationpb"
Expand All @@ -45,23 +48,54 @@ go run cmd/fetch_ca_cert/fetch_ca_cert.go \
--gcp-ca-parent="projects/<project>/locations/<region>/caPools/<ca-pool>" \
--output="chain.crt.pem"
go run cmd/fetch_ca_cert/fetch_ca_cert.go \
--tink-kms-resource="gcp-kms://projects/<project>/locations/<region>/keyRings/<key-ring>/cryptoKeys/<key>" \
--tink-keyset-path="enc-keyset.cfg" \
--gcp-ca-parent="projects/<project>/locations/<region>/caPools/<ca-pool>" \
--output="chain.crt.pem"
You must have the permissions to read the KMS key, and create a certificate in the CA pool.
*/

var (
gcpCaParent = flag.String("gcp-ca-parent", "", "Resource path to GCP CA Service CA")
kmsKey = flag.String("kms-resource", "", "Resource path to KMS key, starting with gcpkms://, awskms://, azurekms:// or hashivault://")
outputPath = flag.String("output", "", "Path to the output file")
gcpCaParent = flag.String("gcp-ca-parent", "", "Resource path to GCP CA Service CA")
kmsKey = flag.String("kms-resource", "", "Resource path to KMS key, starting with gcpkms://, awskms://, azurekms:// or hashivault://")
tinkKeysetPath = flag.String("tink-keyset-path", "", "Path to Tink keyset")
tinkKmsKey = flag.String("tink-kms-resource", "", "Resource path to KMS key to decrypt Tink keyset, starting with gcp-kms:// or aws-kms://")
outputPath = flag.String("output", "", "Path to the output file")
)

func fetchCACertificate(ctx context.Context, parent, kmsKey string, client *privateca.CertificateAuthorityClient) ([]*x509.Certificate, error) {
kmsSigner, err := kms.Get(ctx, kmsKey, crypto.SHA256)
if err != nil {
return nil, err
}
signer, _, err := kmsSigner.CryptoSigner(ctx, func(err error) {})
if err != nil {
return nil, err
func fetchCACertificate(ctx context.Context, parent, kmsKey, tinkKeysetPath, tinkKmsKey string,
client *privateca.CertificateAuthorityClient) ([]*x509.Certificate, error) {
var signer crypto.Signer
if len(kmsKey) > 0 {
kmsSigner, err := kms.Get(ctx, kmsKey, crypto.SHA256)
if err != nil {
return nil, err
}
signer, _, err = kmsSigner.CryptoSigner(ctx, func(err error) {})
if err != nil {
return nil, err
}
} else {
primaryKey, err := tinkca.GetPrimaryKey(tinkKmsKey)
if err != nil {
return nil, err
}
f, err := os.Open(filepath.Clean(tinkKeysetPath))
if err != nil {
return nil, err
}
defer f.Close()

kh, err := keyset.Read(keyset.NewJSONReader(f), primaryKey)
if err != nil {
return nil, err
}
signer, err = tinkca.KeyHandleToSigner(kh)
if err != nil {
return nil, err
}
}

pemPubKey, err := cryptoutils.MarshalPublicKeyToPEM(signer.Public())
Expand Down Expand Up @@ -142,8 +176,11 @@ func main() {
if *gcpCaParent == "" {
log.Fatal("gcp-ca-parent must be set")
}
if *kmsKey == "" {
log.Fatal("kms-resource must be set")
if *kmsKey == "" && *tinkKeysetPath == "" {
log.Fatal("either kms-resource or tink-keyset-path must be set")
}
if *tinkKeysetPath != "" && *tinkKmsKey == "" {
log.Fatal("tink-keyset-path must be set with tink-kms-resource must be set")
}
if *outputPath == "" {
log.Fatal("output must be set")
Expand All @@ -153,7 +190,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
parsedCerts, err := fetchCACertificate(context.Background(), *gcpCaParent, *kmsKey, client)
parsedCerts, err := fetchCACertificate(context.Background(), *gcpCaParent, *kmsKey, *tinkKeysetPath, *tinkKmsKey, client)
if err != nil {
log.Fatal(err)
}
Expand Down
28 changes: 27 additions & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ Configuration:
Be sure to run `gcloud auth application-default login` before `docker-compose up` so that
your credentials are mounted on the container.

### Tink

The Tink signing backend uses an on-disk signer loaded from an encrypted Tink keyset and
certificate chain, where the first certificate in the chain certifies the public key from
the Tink keyset. The Tink keyset must be encrypted with a GCP KMS key, and stored in
a JSON format. The CA can either run as an intermediate CA chaining up to an offline root CA,
or as a root CA.

**Tink keysets use strong security defaults and are the most secure way to store an encryption
key locally.**

The supported Tink keysets are:
* ECDSA P-256, SHA256 hash
* ECDSA P-384, SHA512 hash
* ECDSA P-521, SHA512 hash
* ED25519

Configuration:
* `--ca=tinkca`
* `--tink-kms-resource=gcp-kms://<resource>`, also supporting `aws-kms://`
* `--tink-keyset-path=/...`, a JSON-encoded encrypted Tink keyset
* `--tink-cert-chain-path=/...`, a PEM-encoded certificate chain

Be sure to run `gcloud auth application-default login` before `docker-compose up` so that
your credentials are mounted on the container.

### Google Cloud Platform CA Service

The GCP CA Service signing backend delegates creation and signing of the certificates
Expand Down Expand Up @@ -203,4 +229,4 @@ Set `SIGSTORE_ROOT_FILE` with the path to a PEM-encoded root certificate.
To get the root certificate, call `curl -o fulcio.crt.pem http://localhost:5555/api/v1/rootCert`.

Set `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE` with the path to a PEM or DER-encoded CT log public key.
If using `docker-compose`, the public key is available at `config/ctfe/pubkey.pem`.
If using `docker-compose`, the public key is available at `config/ctfe/pubkey.pem`.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/golang/protobuf v1.5.2
github.com/google/certificate-transparency-go v1.1.3
github.com/google/go-cmp v0.5.8
github.com/google/tink/go v1.6.1
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3
Expand Down
Loading

0 comments on commit 3fda04f

Please sign in to comment.