diff options
| author | Matt Holt <mholt@users.noreply.github.com> | 2020-03-13 11:06:08 -0600 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-03-13 11:06:08 -0600 | 
| commit | 5a19db5dc2db7c02d0f99630a07a64cacb7f7b44 (patch) | |
| tree | d820ee2920d97d7cf2faf0fd9541156e20c88d60 /modules/caddypki | |
| parent | cfe85a9fe625fea55dc4f809fd91b5c061064508 (diff) | |
v2: Implement 'pki' app powered by Smallstep for localhost certificates (#3125)
* pki: Initial commit of PKI app (WIP) (see #2502 and #3021)
* pki: Ability to use root/intermediates, and sign with root
* pki: Fix benign misnamings left over from copy+paste
* pki: Only install root if not already trusted
* Make HTTPS port the default; all names use auto-HTTPS; bug fixes
* Fix build - what happened to our CI tests??
* Fix go.mod
Diffstat (limited to 'modules/caddypki')
| -rw-r--r-- | modules/caddypki/ca.go | 334 | ||||
| -rw-r--r-- | modules/caddypki/certificates.go | 50 | ||||
| -rw-r--r-- | modules/caddypki/command.go | 89 | ||||
| -rw-r--r-- | modules/caddypki/crypto.go | 155 | ||||
| -rw-r--r-- | modules/caddypki/maintain.go | 99 | ||||
| -rw-r--r-- | modules/caddypki/pki.go | 117 | 
6 files changed, 844 insertions, 0 deletions
| diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go new file mode 100644 index 0000000..f15883e --- /dev/null +++ b/modules/caddypki/ca.go @@ -0,0 +1,334 @@ +// Copyright 2015 Matthew Holt and The Caddy 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 caddypki + +import ( +	"crypto/x509" +	"encoding/json" +	"fmt" +	"path" +	"sync" +	"time" + +	"github.com/caddyserver/caddy/v2" +	"github.com/caddyserver/certmagic" +	"go.uber.org/zap" +) + +// CA describes a certificate authority, which consists of +// root/signing certificates and various settings pertaining +// to the issuance of certificates and trusting them. +type CA struct { +	// The user-facing name of the certificate authority. +	Name string `json:"name,omitempty"` + +	// The name to put in the CommonName field of the +	// root certificate. +	RootCommonName string `json:"root_common_name,omitempty"` + +	// The name to put in the CommonName field of the +	// intermediate certificates. +	IntermediateCommonName string `json:"intermediate_common_name,omitempty"` + +	// Whether Caddy will attempt to install the CA's root +	// into the system trust store, as well as into Java +	// and Mozilla Firefox trust stores. Default: true. +	InstallTrust *bool `json:"install_trust,omitempty"` + +	Root         *KeyPair `json:"root,omitempty"` +	Intermediate *KeyPair `json:"intermediate,omitempty"` + +	// Optionally configure a separate storage module associated with this +	// issuer, instead of using Caddy's global/default-configured storage. +	// This can be useful if you want to keep your signing keys in a +	// separate location from your leaf certificates. +	StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` + +	id          string +	storage     certmagic.Storage +	root, inter *x509.Certificate +	interKey    interface{} // TODO: should we just store these as crypto.Signer? +	mu          *sync.RWMutex + +	rootCertPath string // mainly used for logging purposes if trusting +	log          *zap.Logger +} + +// Provision sets up the CA. +func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { +	ca.mu = new(sync.RWMutex) +	ca.log = log.Named("ca." + id) + +	if id == "" { +		return fmt.Errorf("CA ID is required (use 'local' for the default CA)") +	} +	ca.mu.Lock() +	ca.id = id +	ca.mu.Unlock() + +	if ca.StorageRaw != nil { +		val, err := ctx.LoadModule(ca, "StorageRaw") +		if err != nil { +			return fmt.Errorf("loading storage module: %v", err) +		} +		cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() +		if err != nil { +			return fmt.Errorf("creating storage configuration: %v", err) +		} +		ca.storage = cmStorage +	} +	if ca.storage == nil { +		ca.storage = ctx.Storage() +	} + +	if ca.Name == "" { +		ca.Name = defaultCAName +	} +	if ca.RootCommonName == "" { +		ca.RootCommonName = defaultRootCommonName +	} +	if ca.IntermediateCommonName == "" { +		ca.IntermediateCommonName = defaultIntermediateCommonName +	} + +	// load the certs and key that will be used for signing +	var rootCert, interCert *x509.Certificate +	var rootKey, interKey interface{} +	var err error +	if ca.Root != nil { +		if ca.Root.Format == "" || ca.Root.Format == "pem_file" { +			ca.rootCertPath = ca.Root.Certificate +		} +		rootCert, rootKey, err = ca.Root.Load() +	} else { +		ca.rootCertPath = "storage:" + ca.storageKeyRootCert() +		rootCert, rootKey, err = ca.loadOrGenRoot() +	} +	if err != nil { +		return err +	} +	if ca.Intermediate != nil { +		interCert, interKey, err = ca.Intermediate.Load() +	} else { +		interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey) +	} +	if err != nil { +		return err +	} + +	ca.mu.Lock() +	ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey +	ca.mu.Unlock() + +	return nil +} + +// ID returns the CA's ID, as given by the user in the config. +func (ca CA) ID() string { +	return ca.id +} + +// RootCertificate returns the CA's root certificate (public key). +func (ca CA) RootCertificate() *x509.Certificate { +	ca.mu.RLock() +	defer ca.mu.RUnlock() +	return ca.root +} + +// RootKey returns the CA's root private key. Since the root key is +// not cached in memory long-term, it needs to be loaded from storage, +// which could yield an error. +func (ca CA) RootKey() (interface{}, error) { +	_, rootKey, err := ca.loadOrGenRoot() +	return rootKey, err +} + +// IntermediateCertificate returns the CA's intermediate +// certificate (public key). +func (ca CA) IntermediateCertificate() *x509.Certificate { +	ca.mu.RLock() +	defer ca.mu.RUnlock() +	return ca.inter +} + +// IntermediateKey returns the CA's intermediate private key. +func (ca CA) IntermediateKey() interface{} { +	ca.mu.RLock() +	defer ca.mu.RUnlock() +	return ca.interKey +} + +func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { +	rootCertPEM, err := ca.storage.Load(ca.storageKeyRootCert()) +	if err != nil { +		if _, ok := err.(certmagic.ErrNotExist); !ok { +			return nil, nil, fmt.Errorf("loading root cert: %v", err) +		} + +		// TODO: should we require that all or none of the assets are required before overwriting anything? +		rootCert, rootKey, err = ca.genRoot() +		if err != nil { +			return nil, nil, fmt.Errorf("generating root: %v", err) +		} +	} + +	if rootCert == nil { +		rootCert, err = pemDecodeSingleCert(rootCertPEM) +		if err != nil { +			return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err) +		} +	} +	if rootKey == nil { +		rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey()) +		if err != nil { +			return nil, nil, fmt.Errorf("loading root key: %v", err) +		} +		rootKey, err = pemDecodePrivateKey(rootKeyPEM) +		if err != nil { +			return nil, nil, fmt.Errorf("decoding root key: %v", err) +		} +	} + +	return rootCert, rootKey, nil +} + +func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { +	repl := ca.newReplacer() + +	rootCert, rootKey, err = generateRoot(repl.ReplaceAll(ca.RootCommonName, "")) +	if err != nil { +		return nil, nil, fmt.Errorf("generating CA root: %v", err) +	} +	rootCertPEM, err := pemEncodeCert(rootCert.Raw) +	if err != nil { +		return nil, nil, fmt.Errorf("encoding root certificate: %v", err) +	} +	err = ca.storage.Store(ca.storageKeyRootCert(), rootCertPEM) +	if err != nil { +		return nil, nil, fmt.Errorf("saving root certificate: %v", err) +	} +	rootKeyPEM, err := pemEncodePrivateKey(rootKey) +	if err != nil { +		return nil, nil, fmt.Errorf("encoding root key: %v", err) +	} +	err = ca.storage.Store(ca.storageKeyRootKey(), rootKeyPEM) +	if err != nil { +		return nil, nil, fmt.Errorf("saving root key: %v", err) +	} + +	return rootCert, rootKey, nil +} + +func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { +	interCertPEM, err := ca.storage.Load(ca.storageKeyIntermediateCert()) +	if err != nil { +		if _, ok := err.(certmagic.ErrNotExist); !ok { +			return nil, nil, fmt.Errorf("loading intermediate cert: %v", err) +		} + +		// TODO: should we require that all or none of the assets are required before overwriting anything? +		interCert, interKey, err = ca.genIntermediate(rootCert, rootKey) +		if err != nil { +			return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err) +		} +	} + +	if interCert == nil { +		interCert, err = pemDecodeSingleCert(interCertPEM) +		if err != nil { +			return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err) +		} +	} + +	if interKey == nil { +		interKeyPEM, err := ca.storage.Load(ca.storageKeyIntermediateKey()) +		if err != nil { +			return nil, nil, fmt.Errorf("loading intermediate key: %v", err) +		} +		interKey, err = pemDecodePrivateKey(interKeyPEM) +		if err != nil { +			return nil, nil, fmt.Errorf("decoding intermediate key: %v", err) +		} +	} + +	return interCert, interKey, nil +} + +func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { +	repl := ca.newReplacer() + +	rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey()) +	if err != nil { +		return nil, nil, fmt.Errorf("loading root key to sign new intermediate: %v", err) +	} +	rootKey, err = pemDecodePrivateKey(rootKeyPEM) +	if err != nil { +		return nil, nil, fmt.Errorf("decoding root key: %v", err) +	} +	interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey) +	if err != nil { +		return nil, nil, fmt.Errorf("generating CA intermediate: %v", err) +	} +	interCertPEM, err := pemEncodeCert(interCert.Raw) +	if err != nil { +		return nil, nil, fmt.Errorf("encoding intermediate certificate: %v", err) +	} +	err = ca.storage.Store(ca.storageKeyIntermediateCert(), interCertPEM) +	if err != nil { +		return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err) +	} +	interKeyPEM, err := pemEncodePrivateKey(interKey) +	if err != nil { +		return nil, nil, fmt.Errorf("encoding intermediate key: %v", err) +	} +	err = ca.storage.Store(ca.storageKeyIntermediateKey(), interKeyPEM) +	if err != nil { +		return nil, nil, fmt.Errorf("saving intermediate key: %v", err) +	} + +	return interCert, interKey, nil +} + +func (ca CA) storageKeyCAPrefix() string { +	return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.id)) +} +func (ca CA) storageKeyRootCert() string { +	return path.Join(ca.storageKeyCAPrefix(), "root.crt") +} +func (ca CA) storageKeyRootKey() string { +	return path.Join(ca.storageKeyCAPrefix(), "root.key") +} +func (ca CA) storageKeyIntermediateCert() string { +	return path.Join(ca.storageKeyCAPrefix(), "intermediate.crt") +} +func (ca CA) storageKeyIntermediateKey() string { +	return path.Join(ca.storageKeyCAPrefix(), "intermediate.key") +} + +func (ca CA) newReplacer() *caddy.Replacer { +	repl := caddy.NewReplacer() +	repl.Set("pki.ca.name", ca.Name) +	return repl +} + +const ( +	defaultCAID                   = "local" +	defaultCAName                 = "Caddy Local Authority" +	defaultRootCommonName         = "{pki.ca.name} - {time.now.year} ECC Root" +	defaultIntermediateCommonName = "{pki.ca.name} - ECC Intermediate" + +	defaultRootLifetime         = 24 * time.Hour * 30 * 12 * 10 +	defaultIntermediateLifetime = 24 * time.Hour * 7 +) diff --git a/modules/caddypki/certificates.go b/modules/caddypki/certificates.go new file mode 100644 index 0000000..a55c165 --- /dev/null +++ b/modules/caddypki/certificates.go @@ -0,0 +1,50 @@ +// Copyright 2015 Matthew Holt and The Caddy 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 caddypki + +import ( +	"crypto/x509" +	"time" + +	"github.com/smallstep/cli/crypto/x509util" +) + +func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey interface{}, err error) { +	rootProfile, err := x509util.NewRootProfile(commonName) +	if err != nil { +		return +	} +	rootProfile.Subject().NotAfter = time.Now().Add(defaultRootLifetime) // TODO: make configurable +	return newCert(rootProfile) +} + +func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey interface{}) (cert *x509.Certificate, privateKey interface{}, err error) { +	interProfile, err := x509util.NewIntermediateProfile(commonName, rootCrt, rootKey) +	if err != nil { +		return +	} +	interProfile.Subject().NotAfter = time.Now().Add(defaultIntermediateLifetime) // TODO: make configurable +	return newCert(interProfile) +} + +func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey interface{}, err error) { +	certBytes, err := profile.CreateCertificate() +	if err != nil { +		return +	} +	privateKey = profile.SubjectPrivateKey() +	cert, err = x509.ParseCertificate(certBytes) +	return +} diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go new file mode 100644 index 0000000..9276fcb --- /dev/null +++ b/modules/caddypki/command.go @@ -0,0 +1,89 @@ +// Copyright 2015 Matthew Holt and The Caddy 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 caddypki + +import ( +	"flag" +	"fmt" +	"os" +	"path/filepath" + +	"github.com/caddyserver/caddy/v2" +	caddycmd "github.com/caddyserver/caddy/v2/cmd" +	"github.com/smallstep/truststore" +) + +func init() { +	caddycmd.RegisterCommand(caddycmd.Command{ +		Name:  "untrust", +		Func:  cmdUntrust, +		Usage: "[--ca <id> | --cert <path>]", +		Short: "Untrusts a locally-trusted CA certificate", +		Long: ` +Untrusts a root certificate from the local trust store(s). Intended +for development environments only. + +This command uninstalls trust; it does not necessarily delete the +root certificate from trust stores entirely. Thus, repeatedly +trusting and untrusting new certificates can fill up trust databases. + +This command does not delete or modify certificate files. + +Specify which certificate to untrust either by the ID of its CA with +the --ca flag, or the direct path to the certificate file with the +--cert flag. If the --ca flag is used, only the default storage paths +are assumed (i.e. using --ca flag with custom storage backends or file +paths will not work). + +If no flags are specified, --ca=local is assumed.`, +		Flags: func() *flag.FlagSet { +			fs := flag.NewFlagSet("untrust", flag.ExitOnError) +			fs.String("ca", "", "The ID of the CA to untrust") +			fs.String("cert", "", "The path to the CA certificate to untrust") +			return fs +		}(), +	}) +} + +func cmdUntrust(fs caddycmd.Flags) (int, error) { +	ca := fs.String("ca") +	cert := fs.String("cert") + +	if ca != "" && cert != "" { +		return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments") +	} +	if ca == "" && cert == "" { +		ca = defaultCAID +	} +	if ca != "" { +		cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt") +	} + +	// sanity check, make sure cert file exists first +	_, err := os.Stat(cert) +	if err != nil { +		return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err) +	} + +	err = truststore.UninstallFile(cert, +		truststore.WithDebug(), +		truststore.WithFirefox(), +		truststore.WithJava()) +	if err != nil { +		return caddy.ExitCodeFailedStartup, err +	} + +	return caddy.ExitCodeSuccess, nil +} diff --git a/modules/caddypki/crypto.go b/modules/caddypki/crypto.go new file mode 100644 index 0000000..e701c40 --- /dev/null +++ b/modules/caddypki/crypto.go @@ -0,0 +1,155 @@ +// Copyright 2015 Matthew Holt and The Caddy 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 caddypki + +import ( +	"bytes" +	"crypto" +	"crypto/ecdsa" +	"crypto/ed25519" +	"crypto/rsa" +	"crypto/x509" +	"encoding/pem" +	"fmt" +	"io/ioutil" +	"strings" +) + +func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) { +	pemBlock, remaining := pem.Decode(pemDER) +	if pemBlock == nil { +		return nil, fmt.Errorf("no PEM block found") +	} +	if len(remaining) > 0 { +		return nil, fmt.Errorf("input contained more than a single PEM block") +	} +	if pemBlock.Type != "CERTIFICATE" { +		return nil, fmt.Errorf("expected PEM block type to be CERTIFICATE, but got '%s'", pemBlock.Type) +	} +	return x509.ParseCertificate(pemBlock.Bytes) +} + +func pemEncodeCert(der []byte) ([]byte, error) { +	return pemEncode("CERTIFICATE", der) +} + +// pemEncodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes. +// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported. +func pemEncodePrivateKey(key crypto.PrivateKey) ([]byte, error) { +	var pemType string +	var keyBytes []byte +	switch key := key.(type) { +	case *ecdsa.PrivateKey: +		var err error +		pemType = "EC" +		keyBytes, err = x509.MarshalECPrivateKey(key) +		if err != nil { +			return nil, err +		} +	case *rsa.PrivateKey: +		pemType = "RSA" +		keyBytes = x509.MarshalPKCS1PrivateKey(key) +	case *ed25519.PrivateKey: +		var err error +		pemType = "ED25519" +		keyBytes, err = x509.MarshalPKCS8PrivateKey(key) +		if err != nil { +			return nil, err +		} +	default: +		return nil, fmt.Errorf("unsupported key type: %T", key) +	} +	return pemEncode(pemType+" PRIVATE KEY", keyBytes) +} + +// pemDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes. +// Borrowed from Go standard library, to handle various private key and PEM block types. +// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308 +// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238) +// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported. +func pemDecodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) { +	keyBlockDER, _ := pem.Decode(keyPEMBytes) + +	if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") { +		return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type) +	} + +	if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil { +		return key, nil +	} + +	if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil { +		switch key := key.(type) { +		case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: +			return key, nil +		default: +			return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key) +		} +	} + +	if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil { +		return key, nil +	} + +	return nil, fmt.Errorf("unknown private key type") +} + +func pemEncode(blockType string, b []byte) ([]byte, error) { +	var buf bytes.Buffer +	err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: b}) +	return buf.Bytes(), err +} + +func trusted(cert *x509.Certificate) bool { +	chains, err := cert.Verify(x509.VerifyOptions{}) +	return len(chains) > 0 && err == nil +} + +// KeyPair represents a public-private key pair, where the +// public key is also called a certificate. +type KeyPair struct { +	Certificate string `json:"certificate,omitempty"` +	PrivateKey  string `json:"private_key,omitempty"` +	Format      string `json:"format,omitempty"` +} + +// Load loads the certificate and key. +func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) { +	switch kp.Format { +	case "", "pem_file": +		certData, err := ioutil.ReadFile(kp.Certificate) +		if err != nil { +			return nil, nil, err +		} +		keyData, err := ioutil.ReadFile(kp.PrivateKey) +		if err != nil { +			return nil, nil, err +		} + +		cert, err := pemDecodeSingleCert(certData) +		if err != nil { +			return nil, nil, err +		} +		key, err := pemDecodePrivateKey(keyData) +		if err != nil { +			return nil, nil, err +		} + +		return cert, key, nil + +	default: +		return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format) +	} +} diff --git a/modules/caddypki/maintain.go b/modules/caddypki/maintain.go new file mode 100644 index 0000000..2fce0d9 --- /dev/null +++ b/modules/caddypki/maintain.go @@ -0,0 +1,99 @@ +// Copyright 2015 Matthew Holt and The Caddy 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 caddypki + +import ( +	"crypto/x509" +	"fmt" +	"time" + +	"go.uber.org/zap" +) + +func (p *PKI) maintenance() { +	ticker := time.NewTicker(10 * time.Minute) // TODO: make configurable +	defer ticker.Stop() + +	for { +		select { +		case <-ticker.C: +			p.renewCerts() +		case <-p.ctx.Done(): +			return +		} +	} +} + +func (p *PKI) renewCerts() { +	for _, ca := range p.CAs { +		err := p.renewCertsForCA(ca) +		if err != nil { +			p.log.Error("renewing intermediate certificates", +				zap.Error(err), +				zap.String("ca", ca.id)) +		} +	} +} + +func (p *PKI) renewCertsForCA(ca *CA) error { +	ca.mu.Lock() +	defer ca.mu.Unlock() + +	log := p.log.With(zap.String("ca", ca.id)) + +	// only maintain the root if it's not manually provided in the config +	if ca.Root == nil { +		if needsRenewal(ca.root) { +			// TODO: implement root renewal (use same key) +			log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)", +				zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), +			) +		} +	} + +	// only maintain the intermediate if it's not manually provided in the config +	if ca.Intermediate == nil { +		if needsRenewal(ca.inter) { +			log.Info("intermediate expires soon; renewing", +				zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), +			) + +			rootCert, rootKey, err := ca.loadOrGenRoot() +			if err != nil { +				return fmt.Errorf("loading root key: %v", err) +			} +			interCert, interKey, err := ca.genIntermediate(rootCert, rootKey) +			if err != nil { +				return fmt.Errorf("generating new certificate: %v", err) +			} +			ca.inter, ca.interKey = interCert, interKey + +			log.Info("renewed intermediate", +				zap.Time("new_expiration", ca.inter.NotAfter), +			) +		} +	} + +	return nil +} + +func needsRenewal(cert *x509.Certificate) bool { +	lifetime := cert.NotAfter.Sub(cert.NotBefore) +	renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio) +	renewalWindowStart := cert.NotAfter.Add(-renewalWindow) +	return time.Now().After(renewalWindowStart) +} + +const renewalWindowRatio = 0.2 // TODO: make configurable diff --git a/modules/caddypki/pki.go b/modules/caddypki/pki.go new file mode 100644 index 0000000..1b10a8e --- /dev/null +++ b/modules/caddypki/pki.go @@ -0,0 +1,117 @@ +// Copyright 2015 Matthew Holt and The Caddy 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 caddypki + +import ( +	"fmt" + +	"github.com/caddyserver/caddy/v2" +	"github.com/smallstep/truststore" +	"go.uber.org/zap" +) + +func init() { +	caddy.RegisterModule(PKI{}) +} + +// PKI provides Public Key Infrastructure facilities for Caddy. +type PKI struct { +	// The CAs to manage. Each CA is keyed by an ID that is used +	// to uniquely identify it from other CAs. The default CA ID +	// is "local". +	CAs map[string]*CA `json:"certificate_authorities,omitempty"` + +	ctx caddy.Context +	log *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (PKI) CaddyModule() caddy.ModuleInfo { +	return caddy.ModuleInfo{ +		ID:  "pki", +		New: func() caddy.Module { return new(PKI) }, +	} +} + +// Provision sets up the configuration for the PKI app. +func (p *PKI) Provision(ctx caddy.Context) error { +	p.ctx = ctx +	p.log = ctx.Logger(p) + +	// if this app is initialized at all, ensure there's +	// at least a default CA that can be used +	if len(p.CAs) == 0 { +		p.CAs = map[string]*CA{defaultCAID: new(CA)} +	} + +	for caID, ca := range p.CAs { +		err := ca.Provision(ctx, caID, p.log) +		if err != nil { +			return fmt.Errorf("provisioning CA '%s': %v", caID, err) +		} +	} + +	return nil +} + +// Start starts the PKI app. +func (p *PKI) Start() error { +	// install roots to trust store, if not disabled +	for _, ca := range p.CAs { +		if ca.InstallTrust != nil && !*ca.InstallTrust { +			ca.log.Warn("root certificate trust store installation disabled; clients will show warnings without intervention", +				zap.String("path", ca.rootCertPath)) +			continue +		} + +		// avoid password prompt if already trusted +		if trusted(ca.root) { +			ca.log.Info("root certificate is already trusted by system", +				zap.String("path", ca.rootCertPath)) +			continue +		} + +		ca.log.Warn("trusting root certificate (you might be prompted for password)", +			zap.String("path", ca.rootCertPath)) + +		err := truststore.Install(ca.root, +			truststore.WithDebug(), +			truststore.WithFirefox(), +			truststore.WithJava(), +		) +		if err != nil { +			return fmt.Errorf("adding root certificate to trust store: %v", err) +		} +	} + +	// see if root/intermediates need renewal... +	p.renewCerts() + +	// ...and keep them renewed +	go p.maintenance() + +	return nil +} + +// Stop stops the PKI app. +func (p *PKI) Stop() error { +	return nil +} + +// Interface guards +var ( +	_ caddy.Provisioner = (*PKI)(nil) +	_ caddy.App         = (*PKI)(nil) +) | 
