mirror of
https://github.com/caddyserver/caddy.git
synced 2026-02-17 08:40:01 -05:00
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m40s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m41s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m37s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m11s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m24s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m17s
Lint / govulncheck (push) Successful in 1m39s
Lint / dependency-review (push) Failing after 58s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m0s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* pki: add per-CA configurable maintenance_interval and renewal_window_ratio - Add MaintenanceInterval and RenewalWindowRatio to CA struct (JSON + Caddyfile). - Run one maintenance goroutine per CA using its own interval. - needsRenewal uses per-CA RenewalWindowRatio; invalid/zero ratio falls back to defaults. - Caddyfile: maintenance_interval duration, renewal_window_ratio <0-1>. - Tests: TestCA_needsRenewal, TestParsePKIApp for new options. Fixes #7475 * fix codestyle
477 lines
15 KiB
Go
477 lines
15 KiB
Go
// 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"
|
||
"crypto/x509"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io/fs"
|
||
"path"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/caddyserver/certmagic"
|
||
"github.com/smallstep/certificates/authority"
|
||
"github.com/smallstep/certificates/db"
|
||
"github.com/smallstep/truststore"
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/caddyserver/caddy/v2"
|
||
)
|
||
|
||
// 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"`
|
||
|
||
// The lifetime for the intermediate certificates
|
||
IntermediateLifetime caddy.Duration `json:"intermediate_lifetime,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"`
|
||
|
||
// The root certificate to use; if null, one will be generated.
|
||
Root *KeyPair `json:"root,omitempty"`
|
||
|
||
// The intermediate (signing) certificate; if null, one will be generated.
|
||
Intermediate *KeyPair `json:"intermediate,omitempty"`
|
||
|
||
// How often to check if intermediate (and root, when applicable) certificates need renewal.
|
||
// Default: 10m.
|
||
MaintenanceInterval caddy.Duration `json:"maintenance_interval,omitempty"`
|
||
|
||
// The fraction of certificate lifetime (0.0–1.0) after which renewal is attempted.
|
||
// For example, 0.2 means renew when 20% of the lifetime remains (e.g. ~73 days for a 1-year cert).
|
||
// Default: 0.2.
|
||
RenewalWindowRatio float64 `json:"renewal_window_ratio,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"`
|
||
|
||
// The unique config-facing ID of the certificate authority.
|
||
// Since the ID is set in JSON config via object key, this
|
||
// field is exported only for purposes of config generation
|
||
// and module provisioning.
|
||
ID string `json:"-"`
|
||
|
||
storage certmagic.Storage
|
||
root *x509.Certificate
|
||
interChain []*x509.Certificate
|
||
interKey crypto.Signer
|
||
mu *sync.RWMutex
|
||
|
||
rootCertPath string // mainly used for logging purposes if trusting
|
||
log *zap.Logger
|
||
ctx caddy.Context
|
||
}
|
||
|
||
// 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)
|
||
ca.ctx = ctx
|
||
|
||
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
|
||
}
|
||
if ca.IntermediateLifetime == 0 {
|
||
ca.IntermediateLifetime = caddy.Duration(defaultIntermediateLifetime)
|
||
}
|
||
if ca.MaintenanceInterval == 0 {
|
||
ca.MaintenanceInterval = caddy.Duration(defaultMaintenanceInterval)
|
||
}
|
||
if ca.RenewalWindowRatio <= 0 || ca.RenewalWindowRatio > 1 {
|
||
ca.RenewalWindowRatio = defaultRenewalWindowRatio
|
||
}
|
||
|
||
// load the certs and key that will be used for signing
|
||
var rootCert *x509.Certificate
|
||
var rootCertChain, interCertChain []*x509.Certificate
|
||
var rootKey, interKey crypto.Signer
|
||
var err error
|
||
if ca.Root != nil {
|
||
if ca.Root.Format == "" || ca.Root.Format == "pem_file" {
|
||
ca.rootCertPath = ca.Root.Certificate
|
||
}
|
||
rootCertChain, rootKey, err = ca.Root.Load()
|
||
rootCert = rootCertChain[0]
|
||
} else {
|
||
ca.rootCertPath = "storage:" + ca.storageKeyRootCert()
|
||
rootCert, rootKey, err = ca.loadOrGenRoot()
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if ca.Intermediate != nil {
|
||
interCertChain, interKey, err = ca.Intermediate.Load()
|
||
} else {
|
||
actualRootLifetime := time.Until(rootCert.NotAfter)
|
||
if time.Duration(ca.IntermediateLifetime) >= actualRootLifetime {
|
||
return fmt.Errorf("intermediate certificate lifetime must be less than actual root certificate lifetime (%s)", actualRootLifetime)
|
||
}
|
||
|
||
interCertChain, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
ca.mu.Lock()
|
||
ca.root, ca.interChain, ca.interKey = rootCert, interCertChain, interKey
|
||
ca.mu.Unlock()
|
||
|
||
return nil
|
||
}
|
||
|
||
// 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() (crypto.Signer, error) {
|
||
_, rootKey, err := ca.loadOrGenRoot()
|
||
return rootKey, err
|
||
}
|
||
|
||
// IntermediateCertificateChain returns the CA's intermediate
|
||
// certificate chain.
|
||
func (ca CA) IntermediateCertificateChain() []*x509.Certificate {
|
||
ca.mu.RLock()
|
||
defer ca.mu.RUnlock()
|
||
return ca.interChain
|
||
}
|
||
|
||
// IntermediateKey returns the CA's intermediate private key.
|
||
func (ca CA) IntermediateKey() crypto.Signer {
|
||
ca.mu.RLock()
|
||
defer ca.mu.RUnlock()
|
||
return ca.interKey
|
||
}
|
||
|
||
// NewAuthority returns a new Smallstep-powered signing authority for this CA.
|
||
// Note that we receive *CA (a pointer) in this method to ensure the closure within it, which
|
||
// executes at a later time, always has the only copy of the CA so it can access the latest,
|
||
// renewed certificates since NewAuthority was called. See #4517 and #4669.
|
||
func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authority, error) {
|
||
// get the root certificate and the issuer cert+key
|
||
rootCert := ca.RootCertificate()
|
||
|
||
// set up the signer; cert/key which signs the leaf certs
|
||
var signerOption authority.Option
|
||
if authorityConfig.SignWithRoot {
|
||
// if we're signing with root, we can just pass the
|
||
// cert/key directly, since it's unlikely to expire
|
||
// while Caddy is running (long lifetime)
|
||
var issuerCert *x509.Certificate
|
||
var issuerKey crypto.Signer
|
||
issuerCert = rootCert
|
||
var err error
|
||
issuerKey, err = ca.RootKey()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("loading signing key: %v", err)
|
||
}
|
||
signerOption = authority.WithX509Signer(issuerCert, issuerKey)
|
||
} else {
|
||
// if we're signing with intermediate, we need to make
|
||
// sure it's always fresh, because the intermediate may
|
||
// renew while Caddy is running (medium lifetime)
|
||
signerOption = authority.WithX509SignerFunc(func() ([]*x509.Certificate, crypto.Signer, error) {
|
||
issuerChain := ca.IntermediateCertificateChain()
|
||
issuerCert := issuerChain[0]
|
||
issuerKey := ca.IntermediateKey()
|
||
ca.log.Debug("using intermediate signer",
|
||
zap.String("serial", issuerCert.SerialNumber.String()),
|
||
zap.String("not_before", issuerCert.NotBefore.String()),
|
||
zap.String("not_after", issuerCert.NotAfter.String()))
|
||
return issuerChain, issuerKey, nil
|
||
})
|
||
}
|
||
|
||
opts := []authority.Option{
|
||
authority.WithConfig(&authority.Config{
|
||
AuthorityConfig: authorityConfig.AuthConfig,
|
||
}),
|
||
signerOption,
|
||
authority.WithX509RootCerts(rootCert),
|
||
}
|
||
|
||
// Add a database if we have one
|
||
if authorityConfig.DB != nil {
|
||
opts = append(opts, authority.WithDatabase(*authorityConfig.DB))
|
||
}
|
||
auth, err := authority.NewEmbedded(opts...)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("initializing certificate authority: %v", err)
|
||
}
|
||
|
||
return auth, nil
|
||
}
|
||
|
||
func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err error) {
|
||
if ca.Root != nil {
|
||
rootChain, rootSigner, err := ca.Root.Load()
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return rootChain[0], rootSigner, nil
|
||
}
|
||
rootCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootCert())
|
||
if err != nil {
|
||
if !errors.Is(err, fs.ErrNotExist) {
|
||
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 = pemDecodeCertificate(rootCertPEM)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err)
|
||
}
|
||
}
|
||
if rootKey == nil {
|
||
rootKeyPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootKey())
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("loading root key: %v", err)
|
||
}
|
||
rootKey, err = certmagic.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 crypto.Signer, 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.ctx, ca.storageKeyRootCert(), rootCertPEM)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("saving root certificate: %v", err)
|
||
}
|
||
rootKeyPEM, err := certmagic.PEMEncodePrivateKey(rootKey)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("encoding root key: %v", err)
|
||
}
|
||
err = ca.storage.Store(ca.ctx, 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 crypto.Signer) (interCertChain []*x509.Certificate, interKey crypto.Signer, err error) {
|
||
var interCert *x509.Certificate
|
||
interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert())
|
||
if err != nil {
|
||
if !errors.Is(err, fs.ErrNotExist) {
|
||
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)
|
||
}
|
||
|
||
interCertChain = append(interCertChain, interCert)
|
||
}
|
||
|
||
if len(interCertChain) == 0 {
|
||
interCertChain, err = pemDecodeCertificateChain(interCertPEM)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err)
|
||
}
|
||
}
|
||
|
||
if interKey == nil {
|
||
interKeyPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateKey())
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("loading intermediate key: %v", err)
|
||
}
|
||
interKey, err = certmagic.PEMDecodePrivateKey(interKeyPEM)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("decoding intermediate key: %v", err)
|
||
}
|
||
}
|
||
|
||
return interCertChain, interKey, nil
|
||
}
|
||
|
||
func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
|
||
repl := ca.newReplacer()
|
||
|
||
interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey, time.Duration(ca.IntermediateLifetime))
|
||
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.ctx, ca.storageKeyIntermediateCert(), interCertPEM)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err)
|
||
}
|
||
interKeyPEM, err := certmagic.PEMEncodePrivateKey(interKey)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("encoding intermediate key: %v", err)
|
||
}
|
||
err = ca.storage.Store(ca.ctx, 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
|
||
}
|
||
|
||
// installRoot installs this CA's root certificate into the
|
||
// local trust store(s) if it is not already trusted. The CA
|
||
// must already be provisioned.
|
||
func (ca CA) installRoot() error {
|
||
// 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))
|
||
return nil
|
||
}
|
||
|
||
ca.log.Warn("installing root certificate (you might be prompted for password)",
|
||
zap.String("path", ca.rootCertPath))
|
||
|
||
return truststore.Install(ca.root,
|
||
truststore.WithDebug(),
|
||
truststore.WithFirefox(),
|
||
truststore.WithJava(),
|
||
)
|
||
}
|
||
|
||
// AuthorityConfig is used to help a CA configure
|
||
// the underlying signing authority.
|
||
type AuthorityConfig struct {
|
||
SignWithRoot bool
|
||
|
||
// TODO: should we just embed the underlying authority.Config struct type?
|
||
DB *db.AuthDB
|
||
AuthConfig *authority.AuthConfig
|
||
}
|
||
|
||
const (
|
||
// DefaultCAID is the default CA ID.
|
||
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
|
||
defaultMaintenanceInterval = 10 * time.Minute
|
||
defaultRenewalWindowRatio = 0.2
|
||
)
|