Skip to content
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

Add Tink signing backend #645

Merged
merged 1 commit into from
Jun 28, 2022
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
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`.
2 changes: 2 additions & 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 Expand Up @@ -85,6 +86,7 @@ require (
github.com/google/btree v1.0.1 // indirect
github.com/google/go-containerregistry v0.9.0 // indirect
github.com/google/trillian v1.4.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
Expand Down
Loading