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 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 modules/caddypki/ca.go (limited to 'modules/caddypki/ca.go') 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 +) -- cgit v1.2.3