Skip to content

Commit

Permalink
Add intermediate CA implementation with KMS-backed signer
Browse files Browse the repository at this point in the history
This CA implementation will use an on-disk certificate chain and a
remote KMS signer to sign certificates. There is validation on server
startup that the provided chain matches the provided key.

I've also added a utility to generate the intermediate certificate by
calling GCP CA Service. This will be used to set up Fulcio.

This also refactors the code to add an intermediate CA struct that
implements the common methods. This makes it simple to add new
intermediate CA types, with each only needing to provide a method to
fetch a signer and certificate chain.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper committed Apr 9, 2022
1 parent 765a06a commit 2504406
Show file tree
Hide file tree
Showing 13 changed files with 913 additions and 156 deletions.
16 changes: 13 additions & 3 deletions cmd/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/sigstore/fulcio/pkg/ca/ephemeralca"
"github.com/sigstore/fulcio/pkg/ca/fileca"
googlecav1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1"
"github.com/sigstore/fulcio/pkg/ca/kmsca"
"github.com/sigstore/fulcio/pkg/ca/x509ca"
"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/ctl"
Expand All @@ -53,7 +54,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 | ephemeralca (for testing)")
cmd.Flags().String("ca", "", "googleca | 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 @@ -64,6 +65,8 @@ func newServeCmd() *cobra.Command {
cmd.Flags().String("fileca-key", "", "Path to CA encrypted private key")
cmd.Flags().String("fileca-key-passwd", "", "Password to decrypt CA private key")
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("host", "0.0.0.0", "The host on which to serve requests")
cmd.Flags().String("port", "8080", "The port on which to serve requests")

Expand Down Expand Up @@ -101,7 +104,6 @@ func runServeCmd(cmd *cobra.Command, args []string) {
// There's a MarkDeprecated function in cobra/pflags, but it doesn't use log.Logger
log.Logger.Warn("gcp_private_ca_version is deprecated and will soon be removed; please remove it")
}

case "fileca":
if !viper.IsSet("fileca-cert") {
log.Logger.Fatal("fileca-cert must be set to certificate path when using fileca")
Expand All @@ -112,7 +114,13 @@ func runServeCmd(cmd *cobra.Command, args []string) {
if !viper.IsSet("fileca-key-passwd") {
log.Logger.Fatal("fileca-key-passwd must be set to encryption password for private key file when using fileca")
}

case "kmsca":
if !viper.IsSet("kms-resource") {
log.Logger.Fatal("kms-resource must be set when using kmsca")
}
if !viper.IsSet("kms-cert-chain-path") {
log.Logger.Fatal("kms-cert-chain-path must be set when using kmsca")
}
case "ephemeralca":
// this is a no-op since this is a self-signed in-memory CA for testing
default:
Expand Down Expand Up @@ -153,6 +161,8 @@ func runServeCmd(cmd *cobra.Command, args []string) {
baseca, err = fileca.NewFileCA(certFile, keyFile, keyPass, watch)
case "ephemeralca":
baseca, err = ephemeralca.NewEphemeralCA()
case "kmsca":
baseca, err = kmsca.NewKMSCA(cmd.Context(), viper.GetString("kms-resource"), viper.GetString("kms-cert-chain-path"))
default:
err = fmt.Errorf("invalid value for configured CA: %v", baseca)
}
Expand Down
169 changes: 169 additions & 0 deletions cmd/fetch_ca_cert/fetch_ca_cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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 (
"context"
"crypto"
"crypto/x509"
"errors"
"flag"
"log"
"os"
"time"

privateca "cloud.google.com/go/security/privateca/apiv1"
"github.com/sigstore/sigstore/pkg/cryptoutils"
privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1"
"google.golang.org/protobuf/types/known/durationpb"

// Register the provider-specific plugins
"github.com/sigstore/sigstore/pkg/signature/kms"
_ "github.com/sigstore/sigstore/pkg/signature/kms/aws"
_ "github.com/sigstore/sigstore/pkg/signature/kms/azure"
_ "github.com/sigstore/sigstore/pkg/signature/kms/gcp"
_ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault"
)

/*
To run:
go run cmd/fetch_ca_cert/fetch_ca_cert.go \
--kms-resource="gcpkms://projects/<project>/locations/<region>/keyRings/<key-ring>/cryptoKeys/<key>/versions/1" \
--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")
)

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
}

pemPubKey, err := cryptoutils.MarshalPublicKeyToPEM(signer.Public())
if err != nil {
return nil, err
}

isCa := true
// default value of 0 for int32
var maxIssuerPathLength int32

csr := &privatecapb.CreateCertificateRequest{
Parent: parent,
Certificate: &privatecapb.Certificate{
// Default to a very large lifetime - CA Service will truncate the
// lifetime to be no longer than the root's lifetime.
// 20 years (24 hours * 365 days * 20)
Lifetime: durationpb.New(time.Hour * 24 * 365 * 20),
CertificateConfig: &privatecapb.Certificate_Config{
Config: &privatecapb.CertificateConfig{
PublicKey: &privatecapb.PublicKey{
Format: privatecapb.PublicKey_PEM,
Key: pemPubKey,
},
X509Config: &privatecapb.X509Parameters{
KeyUsage: &privatecapb.KeyUsage{
BaseKeyUsage: &privatecapb.KeyUsage_KeyUsageOptions{
CertSign: true,
CrlSign: true,
},
ExtendedKeyUsage: &privatecapb.KeyUsage_ExtendedKeyUsageOptions{
CodeSigning: true,
},
},
CaOptions: &privatecapb.X509Parameters_CaOptions{
IsCa: &isCa,
MaxIssuerPathLength: &maxIssuerPathLength,
},
},
SubjectConfig: &privatecapb.CertificateConfig_SubjectConfig{
Subject: &privatecapb.Subject{
CommonName: "sigstore-intermediate",
Organization: "sigstore.dev",
},
},
},
},
},
}

resp, err := client.CreateCertificate(ctx, csr)
if err != nil {
return nil, err
}

var pemCerts []string
pemCerts = append(pemCerts, resp.PemCertificate)
pemCerts = append(pemCerts, resp.PemCertificateChain...)

var parsedCerts []*x509.Certificate
for _, c := range pemCerts {
certs, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(c))
if err != nil {
return nil, err
}
if len(certs) != 1 {
return nil, errors.New("unexpected number of certificates returned")
}
parsedCerts = append(parsedCerts, certs[0])
}

return parsedCerts, nil
}

func main() {
flag.Parse()

if *gcpCaParent == "" {
log.Fatal("gcp-ca-parent must be set")
}
if *kmsKey == "" {
log.Fatal("kms-resource must be set")
}
if *outputPath == "" {
log.Fatal("output must be set")
}

client, err := privateca.NewCertificateAuthorityClient(context.Background())
if err != nil {
log.Fatal(err)
}
parsedCerts, err := fetchCACertificate(context.Background(), *gcpCaParent, *kmsKey, client)
if err != nil {
log.Fatal(err)
}
pemCerts, err := cryptoutils.MarshalCertificatesToPEM(parsedCerts)
if err != nil {
log.Fatal(err)
}

err = os.WriteFile(*outputPath, pemCerts, 0600)
if err != nil {
log.Fatal(err)
}
}
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ require (
github.com/google/uuid v1.3.0
github.com/hashicorp/golang-lru v0.5.4
github.com/magiconair/properties v1.8.6
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.1
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.33.0
github.com/sigstore/sigstore v1.2.1-0.20220328200116-ef48ee800626
github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.1
Expand Down

0 comments on commit 2504406

Please sign in to comment.