Skip to content

Commit

Permalink
Add file backed certificate authority
Browse files Browse the repository at this point in the history
Loads private key and certificate from disk. Optionally watches
for file changes and loads updated key pair.

Signed-off-by: Nathan Smith <nathan@nfsmith.ca>
  • Loading branch information
nsmith5 committed Dec 16, 2021
1 parent 6ff4fbf commit f6c5497
Show file tree
Hide file tree
Showing 18 changed files with 589 additions and 1 deletion.
19 changes: 18 additions & 1 deletion cmd/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/sigstore/fulcio/pkg/api"
certauth "github.com/sigstore/fulcio/pkg/ca"
"github.com/sigstore/fulcio/pkg/ca/ephemeralca"
"github.com/sigstore/fulcio/pkg/ca/fileca"
googlecav1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1"
googlecav1beta1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1beta1"
"github.com/sigstore/fulcio/pkg/ca/x509ca"
Expand All @@ -42,14 +43,17 @@ func newServeCmd() *cobra.Command {
}

cmd.Flags().String("log_type", "dev", "logger type to use (dev/prod)")
cmd.Flags().String("ca", "", "googleca | pkcs11ca | ephemeralca (for testing)")
cmd.Flags().String("ca", "", "googleca | pkcs11ca | fileca | 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("gcp_private_ca_version", "v1", "private ca version: [v1|v1beta1] (only used with --ca googleca)")
cmd.Flags().String("hsm-caroot-id", "", "HSM ID for Root CA (only used with --ca pkcs11ca)")
cmd.Flags().String("ct-log-url", "http://localhost:6962/test", "host and path (with log prefix at the end) to the ct log")
cmd.Flags().String("config-path", "/etc/fulcio-config/config.json", "path to fulcio config json")
cmd.Flags().String("pkcs11-config-path", "config/crypto11.conf", "path to fulcio pkcs11 config file")
cmd.Flags().String("fileca-cert", "", "Path to CA certificate")
cmd.Flags().String("fileca-key", "", "Path to CA private key")
cmd.Flags().Bool("fileca-watch", true, "Watch filesystem for updates")
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 @@ -77,6 +81,14 @@ func runServeCmd(cmd *cobra.Command, args []string) {
log.Logger.Fatal("gcp_private_ca_parent must be set when using googleca")
}

case "fileca":
if !viper.IsSet("fileca-cert") {
log.Logger.Fatal("fileca-cert must be set to certificate path when using fileca")
}
if !viper.IsSet("fileca-key") {
log.Logger.Fatal("fileca-key must be set to private key path when using fileca")
}

case "ephemeralca":
// this is a no-op since this is a self-signed in-memory CA for testing
default:
Expand Down Expand Up @@ -116,6 +128,11 @@ func runServeCmd(cmd *cobra.Command, args []string) {
params.CAPath = &path
}
baseca, err = x509ca.NewX509CA(params)
case "fileca":
certFile := viper.GetString("fileca-cert")
keyFile := viper.GetString("fileca-key")
watch := viper.GetBool("fileca-watch")
baseca, err = fileca.NewFileCA(certFile, keyFile, watch)
case "ephemeralca":
baseca, err = ephemeralca.NewEphemeralCA()
default:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/PaesslerAG/jsonpath v0.1.1
github.com/ThalesIgnite/crypto11 v1.2.5
github.com/coreos/go-oidc/v3 v3.1.0
github.com/fsnotify/fsnotify v1.5.1
github.com/go-chi/chi v4.1.2+incompatible
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-cmp v0.5.6
Expand Down
101 changes: 101 additions & 0 deletions pkg/ca/fileca/fileca.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2021 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 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"
)

type fileCA struct {
sync.RWMutex

cert *x509.Certificate
key crypto.Signer
}

// NewFileCA returns a file backed certificate authority. Expects paths to a
// certificate and key that are PEM encoded and not password protected.
func NewFileCA(certPath, keyPath string, watch bool) (ca.CertificateAuthority, error) {
var fca fileCA

var err error
fca.cert, fca.key, err = loadKeyPair(certPath, keyPath)
if err != nil {
return nil, err
}

if watch {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
err = watcher.Add(certPath)
if err != nil {
return nil, err
}
err = watcher.Add(keyPath)
if err != nil {
return nil, err
}

go ioWatch(certPath, keyPath, watcher, fca.updateX509KeyPair)
}

return &fca, err
}

func (fca *fileCA) updateX509KeyPair(cert *x509.Certificate, key 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.cert = cert
fca.key = key
}

func (fca *fileCA) getX509KeyPair() (*x509.Certificate, crypto.Signer) {
fca.RLock()
defer fca.RUnlock()
return fca.cert, 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
}

rootCA, privateKey := fca.getX509KeyPair()

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

return ca.CreateCSCFromDER(subject, finalCertBytes, nil)
}
64 changes: 64 additions & 0 deletions pkg/ca/fileca/fileca_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2021 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 fileca

import (
"crypto/ecdsa"
"crypto/ed25519"
"testing"
)

func TestNewFileCA(t *testing.T) {
_, err := NewFileCA(`testdata/ed25519-cert.pem`, `testdata/ed25519-key.pem`, false)
if err != nil {
t.Error(`Failed to load file CA from disk`)
}
}

func TestCertUpdate(t *testing.T) {
oldCert := `testdata/ed25519-cert.pem`
oldKey := `testdata/ed25519-key.pem`
newCert := `testdata/ecdsa-cert.pem`
newKey := `testdata/ecdsa-key.pem`
watch := false

ca, err := NewFileCA(oldCert, oldKey, watch)
if err != nil {
t.Fatal(`Failed to load file CA from disk`)
}

fca, ok := ca.(*fileCA)
if !ok {
t.Fatal(`Bad CA type`)
}

_, key := fca.getX509KeyPair()
if _, ok = key.(ed25519.PrivateKey); !ok {
t.Error(`first key should have been an ed25519 key`)
}

cert, key, err := loadKeyPair(newCert, newKey)
if err != nil {
t.Fatal(`Failed to load new keypair`)
}

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

if _, ok = key.(*ecdsa.PrivateKey); !ok {
t.Fatal(`file CA should have been updated with ecdsa key`)
}
}
61 changes: 61 additions & 0 deletions pkg/ca/fileca/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2021 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 fileca

import (
"crypto"
"crypto/tls"
"crypto/x509"
"errors"
)

func loadKeyPair(certPath, keyPath string) (*x509.Certificate, crypto.Signer, error) {
tlsCert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, nil, err
}

var (
key crypto.Signer
cert *x509.Certificate
)
{
var ok bool
key, ok = tlsCert.PrivateKey.(crypto.Signer)
if !ok {
// Shouldn't be reachable because of the data validation that
// tls.LoadX509KeyPair already did
return nil, nil, errors.New(`fileca: loaded private key can't be used to sign`)
}

if len(tlsCert.Certificate) != 1 {
return nil, nil, errors.New(`fileca: expected certificate chain with exactly one cert`)
}

cert, err = x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
// Also shouldn't be reachable because tls.LoadX509KeyPair
// already parsed this cert to validate it.
return nil, nil, err
}
}

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

return cert, key, nil
}
56 changes: 56 additions & 0 deletions pkg/ca/fileca/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2021 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 fileca

import (
"fmt"
"testing"
)

func TestValidLoadKeyPair(t *testing.T) {
keypairs := []string{
"ecdsa",
"ed25519",
"rsa4096",
}

for _, keypair := range keypairs {
keyPath := fmt.Sprintf("testdata/%s-key.pem", keypair)
certPath := fmt.Sprintf("testdata/%s-cert.pem", keypair)

_, _, err := loadKeyPair(certPath, keyPath)
if err != nil {
t.Errorf("Failed to load key pair of type %s", keypair)
}
}
}

func TestInvalidLoadKeyPair(t *testing.T) {
keypairs := []string{
"notca",
"mismatch",
}

for _, keypair := range keypairs {
keyPath := fmt.Sprintf("testdata/%s-key.pem", keypair)
certPath := fmt.Sprintf("testdata/%s-cert.pem", keypair)

_, _, err := loadKeyPair(certPath, keyPath)
if err == nil {
t.Errorf("Expected invalid key pair of type %s to fail to load", keypair)
}
}
}
12 changes: 12 additions & 0 deletions pkg/ca/fileca/testdata/ecdsa-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIByDCCAU6gAwIBAgIUQGIeaoDYeu1gbfZDZCbA4tRfcBgwCgYIKoZIzj0EAwIw
EDEOMAwGA1UEAwwFZWNkc2EwIBcNMjExMjE2MDI0NjAyWhgPMjEyMTExMjIwMjQ2
MDJaMBAxDjAMBgNVBAMMBWVjZHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEDBCL
v9Z3ESXJK5aJ1z2iJ2pMuSByDuTGDCdJZ7uZw9DCrZvuwpo69CHUSf7a1Hm1Ly0G
q0Vanuhm5gxHly7LoGWU95wWpgs+uLnXlpEIsEiO7JOUWruBb1IfFYet5jPEo2cw
ZTAdBgNVHQ4EFgQUrhwW+5E6odLZfdJjkE0T4BQYIaQwHwYDVR0jBBgwFoAUrhwW
+5E6odLZfdJjkE0T4BQYIaQwDwYDVR0TAQH/BAUwAwEB/zASBgNVHRMBAf8ECDAG
AQH/AgEBMAoGCCqGSM49BAMCA2gAMGUCMGwGmkqlYRYaUTsSAITMwjdfqd1QfCg/
M9OY6NJd8MMAc0FgLT9mliTcnlP6NB0+uQIxAM86EGJkdqTf0fs4TNJut9dEu+Sb
tvwHCNsV8/WeNbcX7WI/6TEO1ln9EpVEhFKT/A==
-----END CERTIFICATE-----
6 changes: 6 additions & 0 deletions pkg/ca/fileca/testdata/ecdsa-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAoEEUZMXem58C10Mjq
NQtV5AoAHIbUBHlHofzlkB85XoQ/YuGSUJUGm9VUSX9sCaShZANiAAQMEIu/1ncR
JckrlonXPaInaky5IHIO5MYMJ0lnu5nD0MKtm+7Cmjr0IdRJ/trUebUvLQarRVqe
6GbmDEeXLsugZZT3nBamCz64udeWkQiwSI7sk5Rau4FvUh8Vh63mM8Q=
-----END PRIVATE KEY-----
10 changes: 10 additions & 0 deletions pkg/ca/fileca/testdata/ed25519-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBTzCCAQGgAwIBAgIUEvRW73iZMiv+Uj9/TeIlBjMXjqUwBQYDK2VwMBIxEDAO
BgNVBAMMB2VkMjU1MTkwIBcNMjExMjE2MDI0MTA4WhgPMjEyMTExMjIwMjQxMDha
MBIxEDAOBgNVBAMMB2VkMjU1MTkwKjAFBgMrZXADIQASlHPcRjDzpeZzxulCYLBr
vNUpIY+U/VRQWX2bVOGt5aNnMGUwHQYDVR0OBBYEFLZsJymHlUHELUFEqQ/Ct+oW
hhF5MB8GA1UdIwQYMBaAFLZsJymHlUHELUFEqQ/Ct+oWhhF5MA8GA1UdEwEB/wQF
MAMBAf8wEgYDVR0TAQH/BAgwBgEB/wIBATAFBgMrZXADQQDRWFn9bb6lnov5MF02
Agu9UyyPHq9icGBrEzgo+XKD5IbWI47WkaMkwb5teKX/soUuvBdpx01A2mjLIldT
EG8P
-----END CERTIFICATE-----
3 changes: 3 additions & 0 deletions pkg/ca/fileca/testdata/ed25519-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJupNKg2i5A9z95BaXw6O9DAii4Jw9dqV1qs3i4ojPsb
-----END PRIVATE KEY-----
10 changes: 10 additions & 0 deletions pkg/ca/fileca/testdata/mismatch-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBUTCCAQOgAwIBAgIUaw66azMJaY94U7BtBZ7c/XYdl0YwBQYDK2VwMBMxETAP
BgNVBAMMCG1pc21hdGNoMCAXDTIxMTIxNjAyNDczM1oYDzIxMjExMTIyMDI0NzMz
WjATMREwDwYDVQQDDAhtaXNtYXRjaDAqMAUGAytlcAMhAOqTJbZpBETmbyPdx2oF
Yux9k0okt6wyOVykT9k3SBzPo2cwZTAdBgNVHQ4EFgQU+KMr0c2O0ONyvYV8wTDw
HD/yBywwHwYDVR0jBBgwFoAU+KMr0c2O0ONyvYV8wTDwHD/yBywwDwYDVR0TAQH/
BAUwAwEB/zASBgNVHRMBAf8ECDAGAQH/AgEBMAUGAytlcANBAEIfbtgklrBjV6H5
/UyHlegf9Dms4W34qPkv7UrTwGkMay13And04fQUhgQ/M0B/WzV2LEoaL9Vk/08N
f4zT3w0=
-----END CERTIFICATE-----
3 changes: 3 additions & 0 deletions pkg/ca/fileca/testdata/mismatch-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJupNKg2i5A9z95BaXw6O9DAii4Jw9dqV1qs3i4ojPsb
-----END PRIVATE KEY-----

0 comments on commit f6c5497

Please sign in to comment.