From 5a19db5dc2db7c02d0f99630a07a64cacb7f7b44 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 13 Mar 2020 11:06:08 -0600 Subject: 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 --- modules/caddypki/ca.go | 334 +++++++++++++++++++++++++++++++++++++++ modules/caddypki/certificates.go | 50 ++++++ modules/caddypki/command.go | 89 +++++++++++ modules/caddypki/crypto.go | 155 ++++++++++++++++++ modules/caddypki/maintain.go | 99 ++++++++++++ modules/caddypki/pki.go | 117 ++++++++++++++ 6 files changed, 844 insertions(+) create mode 100644 modules/caddypki/ca.go create mode 100644 modules/caddypki/certificates.go create mode 100644 modules/caddypki/command.go create mode 100644 modules/caddypki/crypto.go create mode 100644 modules/caddypki/maintain.go create mode 100644 modules/caddypki/pki.go (limited to 'modules/caddypki') 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 | --cert ]", + 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) +) -- cgit v1.2.3