summaryrefslogtreecommitdiff
path: root/modules/caddypki
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2020-03-13 11:06:08 -0600
committerGitHub <noreply@github.com>2020-03-13 11:06:08 -0600
commit5a19db5dc2db7c02d0f99630a07a64cacb7f7b44 (patch)
treed820ee2920d97d7cf2faf0fd9541156e20c88d60 /modules/caddypki
parentcfe85a9fe625fea55dc4f809fd91b5c061064508 (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.go334
-rw-r--r--modules/caddypki/certificates.go50
-rw-r--r--modules/caddypki/command.go89
-rw-r--r--modules/caddypki/crypto.go155
-rw-r--r--modules/caddypki/maintain.go99
-rw-r--r--modules/caddypki/pki.go117
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)
+)