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 1, 2022
1 parent 765a06a commit 6d2e577
Show file tree
Hide file tree
Showing 13 changed files with 940 additions and 152 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-key", "", "KMS key resource path")
cmd.Flags().String("cert-chain-path", "", "Path to PEM-encoded CA certificate chain")
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-key") {
log.Logger.Fatal("kms-key must be set when using kmsca")
}
if !viper.IsSet("cert-chain-path") {
log.Logger.Fatal("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-key"), viper.GetString("cert-chain-path"))
default:
err = fmt.Errorf("invalid value for configured CA: %v", baseca)
}
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
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.20220330193110-d7475aecf1db
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.1
Expand All @@ -31,3 +31,5 @@ require (
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

replace github.com/sigstore/sigstore v1.2.1-0.20220330193110-d7475aecf1db => github.com/haydentherapper/sigstore v1.1.1-0.20220331234123-c28e9d036bd4
74 changes: 66 additions & 8 deletions go.sum

Large diffs are not rendered by default.

50 changes: 6 additions & 44 deletions pkg/ca/fileca/fileca.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,16 @@
package fileca

import (
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"sync"

"github.com/fsnotify/fsnotify"
"github.com/sigstore/fulcio/pkg/ca"
"github.com/sigstore/fulcio/pkg/ca/x509ca"
"github.com/sigstore/fulcio/pkg/challenges"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/fulcio/pkg/ca/intermediateca"
)

type fileCA struct {
sync.RWMutex

certs []*x509.Certificate
key crypto.Signer
intermediateca.IntermediateCA
}

// NewFileCA returns a file backed certificate authority. Expects paths to a
Expand All @@ -43,7 +35,7 @@ func NewFileCA(certPath, keyPath, keyPass string, watch bool) (ca.CertificateAut
var fca fileCA

var err error
fca.certs, fca.key, err = loadKeyPair(certPath, keyPath, keyPass)
fca.Certs, fca.Signer, err = loadKeyPair(certPath, keyPath, keyPass)
if err != nil {
return nil, err
}
Expand All @@ -68,43 +60,13 @@ func NewFileCA(certPath, keyPath, keyPass string, watch bool) (ca.CertificateAut
return &fca, err
}

func (fca *fileCA) updateX509KeyPair(certs []*x509.Certificate, key crypto.Signer) {
func (fca *fileCA) updateX509KeyPair(certs []*x509.Certificate, signer crypto.Signer) {
fca.Lock()
defer fca.Unlock()

// NB: We use the RWLock to unsure a reading thread can't get a mismatching
// cert / key pair by reading the attributes halfway through the update
// below.
fca.certs = certs
fca.key = key
}

func (fca *fileCA) getX509KeyPair() ([]*x509.Certificate, crypto.Signer) {
fca.RLock()
defer fca.RUnlock()
return fca.certs, fca.key
}

// CreateCertificate issues code signing certificates
func (fca *fileCA) CreateCertificate(_ context.Context, subject *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) {
cert, err := x509ca.MakeX509(subject)
if err != nil {
return nil, err
}

certChain, privateKey := fca.getX509KeyPair()

finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, certChain[0], subject.PublicKey, privateKey)
if err != nil {
return nil, err
}

return ca.CreateCSCFromDER(subject, finalCertBytes, certChain)
}

func (fca *fileCA) Root(ctx context.Context) ([]byte, error) {
fca.RLock()
defer fca.RUnlock()

return cryptoutils.MarshalCertificatesToPEM(fca.certs)
fca.Certs = certs
fca.Signer = signer
}
4 changes: 2 additions & 2 deletions pkg/ca/fileca/fileca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestCertUpdate(t *testing.T) {
t.Fatal(`Bad CA type`)
}

_, key := fca.getX509KeyPair()
key := fca.Signer
if _, ok = key.(ed25519.PrivateKey); !ok {
t.Error(`first key should have been an ed25519 key`)
}
Expand All @@ -68,7 +68,7 @@ func TestCertUpdate(t *testing.T) {
}

fca.updateX509KeyPair(cert, key)
_, key = fca.getX509KeyPair()
key = fca.Signer

if _, ok = key.(*ecdsa.PrivateKey); !ok {
t.Fatal(`file CA should have been updated with ecdsa key`)
Expand Down
98 changes: 10 additions & 88 deletions pkg/ca/fileca/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,71 +18,30 @@ package fileca
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"errors"
"os"
"path/filepath"

"github.com/sigstore/fulcio/pkg/ca/intermediateca"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"go.step.sm/crypto/pemutil"
)

func loadKeyPair(certPath, keyPath, keyPass string) ([]*x509.Certificate, crypto.Signer, error) {

var (
certs []*x509.Certificate
err error
key crypto.Signer
)

// NB: certs are ordered from leaf at certs[0] to root at
// certs[len(certs)-1]
certs, err = pemutil.ReadCertificateBundle(certPath)
data, err := os.ReadFile(filepath.Clean(certPath))
if err != nil {
return nil, nil, err
}

// Verify certificate chain
{
roots := x509.NewCertPool()
roots.AddCert(certs[len(certs)-1])

intermediates := x509.NewCertPool()
if len(certs) > 2 {
for _, intermediate := range certs[1 : len(certs)-1] {
intermediates.AddCert(intermediate)
}
}

opts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageCodeSigning,
},
}
if _, err := certs[0].Verify(opts); err != nil {
return nil, nil, err
}

if !certs[0].IsCA {
return nil, nil, errors.New(`fileca: certificate is not a CA`)
}

// If using an intermediate, verify that code signing extended key
// usage is set to satify extended key usage chainging
if len(certs) > 1 {
var hasExtKeyUsageCodeSigning bool
for _, extKeyUsage := range certs[0].ExtKeyUsage {
if extKeyUsage == x509.ExtKeyUsageCodeSigning {
hasExtKeyUsageCodeSigning = true
break
}
}
if !hasExtKeyUsageCodeSigning {
return nil, nil, errors.New(`fileca: certificate must have extended key usage code signing set to sign code signing certificates`)
}
}
certs, err = cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(data))
if err != nil {
return nil, nil, err
}

{
Expand All @@ -98,46 +57,9 @@ func loadKeyPair(certPath, keyPath, keyPass string) ([]*x509.Certificate, crypto
}
}

if !valid(certs[0], key) {
return nil, nil, errors.New(`fileca: certificate public key and private key don't match`)
if err := intermediateca.VerifyCertChain(certs, key); err != nil {
return nil, nil, err
}

return certs, key, nil
}

func valid(cert *x509.Certificate, key crypto.Signer) bool {
if cert == nil || key == nil {
return false
}

switch pub := cert.PublicKey.(type) {
case *rsa.PublicKey:
priv, ok := key.(*rsa.PrivateKey)
if !ok {
return false
}
if pub.N.Cmp(priv.N) != 0 {
return false
}
case *ecdsa.PublicKey:
priv, ok := key.(*ecdsa.PrivateKey)
if !ok {
return false
}
if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 {
return false
}
case ed25519.PublicKey:
priv, ok := key.(ed25519.PrivateKey)
if !ok {
return false
}
if !bytes.Equal(priv.Public().(ed25519.PublicKey), pub) {
return false
}
default:
return false
}

return true
}
8 changes: 2 additions & 6 deletions pkg/ca/fileca/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,13 @@ func cp(src, dst string) error {
}

func TestIOWatch(t *testing.T) {
dir, err := os.MkdirTemp("", "fileca")
if err != nil {
t.Fatal(`Failed to create temp dir`)
}
// defer os.RemoveAll(dir)
dir := t.TempDir()

keyPath := filepath.Join(dir, "key.pem")
certPath := filepath.Join(dir, "cert.pem")

// Copy initial certs into place
err = cp("testdata/ed25519-key.pem", keyPath)
err := cp("testdata/ed25519-key.pem", keyPath)
if err != nil {
t.Fatal(`Couldn't copy test data to temp file`)
}
Expand Down
Loading

0 comments on commit 6d2e577

Please sign in to comment.