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/caddyhttp/autohttps.go | 160 +++++++++----- modules/caddyhttp/caddyhttp.go | 10 +- modules/caddyhttp/fileserver/command.go | 7 +- modules/caddyhttp/reverseproxy/command.go | 8 +- 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 +++++++++++ modules/caddytls/acmeissuer.go | 5 +- modules/caddytls/internalissuer.go | 199 ++++++++++++++++++ modules/caddytls/tls.go | 52 ++++- modules/standard/import.go | 1 + 14 files changed, 1208 insertions(+), 78 deletions(-) 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 create mode 100644 modules/caddytls/internalissuer.go (limited to 'modules') diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 7dab359..6a23ca0 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" @@ -130,8 +131,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v", srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err) } - if certmagic.HostQualifies(d) && - !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) { + if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) { serverDomainSet[d] = struct{}{} } } @@ -161,6 +161,15 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er ) continue } + + // most clients don't accept wildcards like *.tld... we + // can handle that, but as a courtesy, warn the user + if strings.Contains(d, "*") && + strings.Count(strings.Trim(d, "."), ".") == 1 { + app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)", + zap.String("domain", d)) + } + uniqueDomainsForCerts[d] = struct{}{} } } @@ -202,12 +211,18 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // we now have a list of all the unique names for which we need certs; // turn the set into a slice so that phase 2 can use it app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) + var internal, external []string for d := range uniqueDomainsForCerts { + if certmagic.SubjectQualifiesForPublicCert(d) { + external = append(external, d) + } else { + internal = append(internal, d) + } app.allCertDomains = append(app.allCertDomains, d) } // ensure there is an automation policy to handle these certs - err := app.createAutomationPolicy(ctx) + err := app.createAutomationPolicies(ctx, external, internal) if err != nil { return err } @@ -354,23 +369,29 @@ redirServersLoop: return nil } -// createAutomationPolicy ensures that certificates for this app are -// managed properly; for example, it's implied that the HTTPPort -// should also be the port the HTTP challenge is solved on; the same -// for HTTPS port and TLS-ALPN challenge also. We need to tell the -// TLS app to manage these certs by honoring those port configurations, -// so we either find an existing matching automation policy with an -// ACME issuer, or make a new one and append it. -func (app *App) createAutomationPolicy(ctx caddy.Context) error { +// createAutomationPolicy ensures that automated certificates for this +// app are managed properly. This adds up to two automation policies: +// one for the public names, and one for the internal names. If a catch-all +// automation policy exists, it will be shallow-copied and used as the +// base for the new ones (this is important for preserving behavior the +// user intends to be "defaults"). +func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, internalNames []string) error { + // nothing to do if no names to manage certs for + if len(publicNames) == 0 && len(internalNames) == 0 { + return nil + } + + // start by finding a base policy that the user may have defined + // which should, in theory, apply to any policies derived from it; + // typically this would be a "catch-all" policy with no host filter var matchingPolicy *caddytls.AutomationPolicy - var acmeIssuer *caddytls.ACMEIssuer if app.tlsApp.Automation != nil { - // maybe we can find an exisitng one that matches; this is - // useful if the user made a single automation policy to - // set the CA endpoint to a test/staging endpoint (very - // common), but forgot to customize the ports here, while - // setting them in the HTTP app instead (I did this too - // many times) + // if an existing policy matches (specifically, a catch-all policy), + // we should inherit from it, because that is what the user expects; + // this is very common for user setting a default issuer, with a + // custom CA endpoint, for example - whichever one we choose must + // have a host list that is a superset of the policy we make... + // the policy with no host filter is guaranteed to qualify for _, ap := range app.tlsApp.Automation.Policies { if len(ap.Hosts) == 0 { matchingPolicy = ap @@ -378,51 +399,78 @@ func (app *App) createAutomationPolicy(ctx caddy.Context) error { } } } - if matchingPolicy != nil { - // if it has an ACME issuer, maybe we can just use that - acmeIssuer, _ = matchingPolicy.Issuer.(*caddytls.ACMEIssuer) - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.HTTP == nil { - acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig) - } - if acmeIssuer.Challenges.HTTP.AlternatePort == 0 { - // don't overwrite existing explicit config - acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort - } - if acmeIssuer.Challenges.TLSALPN == nil { - acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig) + if matchingPolicy == nil { + matchingPolicy = new(caddytls.AutomationPolicy) } - if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 { - // don't overwrite existing explicit config - acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort + + // addPolicy adds an automation policy that uses issuer for hosts. + addPolicy := func(issuer certmagic.Issuer, hosts []string) error { + // shallow-copy the matching policy; we want to inherit + // from it, not replace it... this takes two lines to + // overrule compiler optimizations + policyCopy := *matchingPolicy + newPolicy := &policyCopy + + // very important to provision it, since we are + // bypassing the JSON-unmarshaling step + if prov, ok := issuer.(caddy.Provisioner); ok { + err := prov.Provision(ctx) + if err != nil { + return err + } + } + newPolicy.Issuer = issuer + newPolicy.Hosts = hosts + + return app.tlsApp.AddAutomationPolicy(newPolicy) } - if matchingPolicy == nil { - // if there was no matching policy, we'll have to append our own - err := app.tlsApp.AddAutomationPolicy(&caddytls.AutomationPolicy{ - Hosts: app.allCertDomains, - Issuer: acmeIssuer, - }) - if err != nil { + if len(publicNames) > 0 { + var acmeIssuer *caddytls.ACMEIssuer + // if it has an ACME issuer, maybe we can just use that + // TODO: we might need a deep copy here, like a Clone() method on ACMEIssuer... + acmeIssuer, _ = matchingPolicy.Issuer.(*caddytls.ACMEIssuer) + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if app.HTTPPort > 0 || app.HTTPSPort > 0 { + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + } + if app.HTTPPort > 0 { + if acmeIssuer.Challenges.HTTP == nil { + acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig) + } + // don't overwrite existing explicit config + if acmeIssuer.Challenges.HTTP.AlternatePort == 0 { + acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort + } + } + if app.HTTPSPort > 0 { + if acmeIssuer.Challenges.TLSALPN == nil { + acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig) + } + // don't overwrite existing explicit config + if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 { + acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort + } + } + if err := addPolicy(acmeIssuer, publicNames); err != nil { return err } - } else { - // if there was an existing matching policy, we need to reprovision - // its issuer (because we just changed its port settings and it has - // to re-build its stored certmagic config template with the new - // values), then re-assign the Issuer pointer on the policy struct - // because our type assertion changed the address - err := acmeIssuer.Provision(ctx) - if err != nil { + } + + if len(internalNames) > 0 { + internalIssuer := new(caddytls.InternalIssuer) + if err := addPolicy(internalIssuer, internalNames); err != nil { return err } - matchingPolicy.Issuer = acmeIssuer + } + + err := app.tlsApp.Validate() + if err != nil { + return err } return nil diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 6ad70f5..06719b5 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -286,8 +286,8 @@ func (app *App) Start() error { } // enable TLS if there is a policy and if this is not the HTTP port - if len(srv.TLSConnPolicies) > 0 && - int(listenAddr.StartPort+portOffset) != app.httpPort() { + useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() + if useTLS { // create TLS listener tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx) ln = tls.NewListener(ln, tlsCfg) @@ -317,6 +317,12 @@ func (app *App) Start() error { ///////// } + app.logger.Debug("starting server loop", + zap.String("address", lnAddr), + zap.Bool("http3", srv.ExperimentalHTTP3), + zap.Bool("tls", useTLS), + ) + go s.Serve(ln) app.servers = append(app.servers, s) } diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go index fa6560b..18e9be3 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -23,7 +23,6 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/certmagic" @@ -90,11 +89,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { Routes: caddyhttp.RouteList{route}, } if listen == "" { - if certmagic.HostQualifies(domain) { - listen = ":" + strconv.Itoa(certmagic.HTTPSPort) - } else { - listen = ":" + httpcaddyfile.DefaultPort - } + listen = ":" + strconv.Itoa(certmagic.HTTPSPort) } server.Listen = []string{listen} diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 6f70d14..6110ca8 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -25,11 +25,9 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" - "github.com/caddyserver/certmagic" ) func init() { @@ -67,7 +65,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { changeHost := fs.Bool("change-host-header") if from == "" { - from = "localhost:" + httpcaddyfile.DefaultPort + from = "localhost:443" } // URLs need a scheme in order to parse successfully @@ -129,11 +127,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { } } - listen := ":80" + listen := ":443" if urlPort := fromURL.Port(); urlPort != "" { listen = ":" + urlPort - } else if certmagic.HostQualifies(urlHost) { - listen = ":443" } server := &caddyhttp.Server{ 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) +) diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 36fd76c..f108d72 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -145,7 +145,7 @@ func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) { } // PreCheck implements the certmagic.PreChecker interface. -func (m *ACMEIssuer) PreCheck(names []string, interactive bool) (skip bool, err error) { +func (m *ACMEIssuer) PreCheck(names []string, interactive bool) error { return certmagic.NewACMEManager(m.magic, m.template).PreCheck(names, interactive) } @@ -200,8 +200,9 @@ type DNSProviderMaker interface { // Interface guards var ( + _ certmagic.PreChecker = (*ACMEIssuer)(nil) _ certmagic.Issuer = (*ACMEIssuer)(nil) _ certmagic.Revoker = (*ACMEIssuer)(nil) - _ certmagic.PreChecker = (*ACMEIssuer)(nil) + _ caddy.Provisioner = (*ACMEIssuer)(nil) _ ConfigSetter = (*ACMEIssuer)(nil) ) diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go new file mode 100644 index 0000000..53a1d00 --- /dev/null +++ b/modules/caddytls/internalissuer.go @@ -0,0 +1,199 @@ +// 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 caddytls + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddypki" + "github.com/caddyserver/certmagic" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/cli/crypto/x509util" +) + +func init() { + caddy.RegisterModule(InternalIssuer{}) +} + +// InternalIssuer is a certificate issuer that generates +// certificates internally using a locally-configured +// CA which can be customized using the `pki` app. +type InternalIssuer struct { + // The ID of the CA to use for signing. The default + // CA ID is "local". The CA can be configured with the + // `pki` app. + CA string `json:"ca,omitempty"` + + // The validity period of certificates. + Lifetime caddy.Duration `json:"lifetime,omitempty"` + + // If true, the root will be the issuer instead of + // the intermediate. This is NOT recommended and should + // only be used when devices/clients do not properly + // validate certificate chains. + SignWithRoot bool `json:"sign_with_root,omitempty"` + + ca *caddypki.CA +} + +// CaddyModule returns the Caddy module information. +func (InternalIssuer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.issuance.internal", + New: func() caddy.Module { return new(InternalIssuer) }, + } +} + +// Provision sets up the issuer. +func (li *InternalIssuer) Provision(ctx caddy.Context) error { + // get a reference to the configured CA + appModule, err := ctx.App("pki") + if err != nil { + return err + } + pkiApp := appModule.(*caddypki.PKI) + if li.CA == "" { + li.CA = defaultInternalCAName + } + ca, ok := pkiApp.CAs[li.CA] + if !ok { + return fmt.Errorf("no certificate authority configured with id: %s", li.CA) + } + li.ca = ca + + // set any other default values + if li.Lifetime == 0 { + li.Lifetime = caddy.Duration(defaultInternalCertLifetime) + } + + return nil +} + +// IssuerKey returns the unique issuer key for the +// confgured CA endpoint. +func (li InternalIssuer) IssuerKey() string { + return li.ca.ID() +} + +// Issue issues a certificate to satisfy the CSR. +func (li InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + // prepare the signing authority + // TODO: eliminate placeholders / needless values + cfg := &authority.Config{ + Address: "placeholder_Address:1", + Root: []string{"placeholder_Root"}, + IntermediateCert: "placeholder_IntermediateCert", + IntermediateKey: "placeholder_IntermediateKey", + DNSNames: []string{"placeholder_DNSNames"}, + AuthorityConfig: &authority.AuthConfig{ + Provisioners: provisioner.List{}, + }, + } + + // get the root certificate and the issuer cert+key + rootCert := li.ca.RootCertificate() + var issuerCert *x509.Certificate + var issuerKey interface{} + if li.SignWithRoot { + issuerCert = rootCert + var err error + issuerKey, err = li.ca.RootKey() + if err != nil { + return nil, fmt.Errorf("loading signing key: %v", err) + } + } else { + issuerCert = li.ca.IntermediateCertificate() + issuerKey = li.ca.IntermediateKey() + } + + auth, err := authority.New(cfg, + authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)), + authority.WithX509RootCerts(rootCert), + ) + if err != nil { + return nil, fmt.Errorf("initializing certificate authority: %v", err) + } + + // ensure issued certificate does not expire later than its issuer + lifetime := time.Duration(li.Lifetime) + if time.Now().Add(lifetime).After(issuerCert.NotAfter) { + // TODO: log this + lifetime = issuerCert.NotAfter.Sub(time.Now()) + } + + certChain, err := auth.Sign(csr, provisioner.Options{}, + profileDefaultDuration(li.Lifetime), + ) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + for _, cert := range certChain { + err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return &certmagic.IssuedCertificate{ + Certificate: buf.Bytes(), + }, nil +} + +// TODO: borrowing from https://github.com/smallstep/certificates/blob/806abb6232a5691198b891d76b9898ea7f269da0/authority/provisioner/sign_options.go#L191-L211 +// as per https://github.com/smallstep/certificates/issues/198. +// profileDefaultDuration is a wrapper against x509util.WithOption to conform +// the SignOption interface. +type profileDefaultDuration time.Duration + +// TODO: is there a better way to set cert lifetimes than copying from the smallstep libs? +func (d profileDefaultDuration) Option(so provisioner.Options) x509util.WithOption { + var backdate time.Duration + notBefore := so.NotBefore.Time() + if notBefore.IsZero() { + notBefore = time.Now().Truncate(time.Second) + backdate = -1 * so.Backdate + } + notAfter := so.NotAfter.RelativeTime(notBefore) + return func(p x509util.Profile) error { + fn := x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(d)) + if err := fn(p); err != nil { + return err + } + crt := p.Subject() + crt.NotBefore = crt.NotBefore.Add(backdate) + return nil + } +} + +const ( + defaultInternalCAName = "local" + defaultInternalCertLifetime = 12 * time.Hour +) + +// Interface guards +var ( + _ caddy.Provisioner = (*InternalIssuer)(nil) + _ certmagic.Issuer = (*InternalIssuer)(nil) +) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 4fa126e..f91229f 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -175,6 +175,26 @@ func (t *TLS) Provision(ctx caddy.Context) error { return nil } +// Validate validates t's configuration. +func (t *TLS) Validate() error { + if t.Automation != nil { + // ensure that host aren't repeated; since only the first + // automation policy is used, repeating a host in the lists + // isn't useful and is probably a mistake + // TODO: test this + hostSet := make(map[string]int) + for i, ap := range t.Automation.Policies { + for _, h := range ap.Hosts { + if first, ok := hostSet[h]; ok { + return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first) + } + hostSet[h] = i + } + } + } + return nil +} + // Start activates the TLS module. func (t *TLS) Start() error { // now that we are running, and all manual certificates have @@ -266,7 +286,10 @@ func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { } // AddAutomationPolicy provisions and adds ap to the list of the app's -// automation policies. +// automation policies. If an existing automation policy exists that has +// fewer hosts in its list than ap does, ap will be inserted before that +// other policy (this helps ensure that ap will be prioritized/chosen +// over, say, a catch-all policy). func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { if t.Automation == nil { t.Automation = new(AutomationConfig) @@ -275,6 +298,16 @@ func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { if err != nil { return err } + for i, other := range t.Automation.Policies { + // if a catch-all policy (or really, any policy with + // fewer names) exists, prioritize this new policy + if len(other.Hosts) < len(ap.Hosts) { + t.Automation.Policies = append(t.Automation.Policies[:i], + append([]*AutomationPolicy{ap}, t.Automation.Policies[i+1:]...)...) + return nil + } + } + // otherwise just append the new one t.Automation.Policies = append(t.Automation.Policies, ap) return nil } @@ -444,6 +477,7 @@ type AutomationPolicy struct { // obtaining or renewing certificates. This is often // not desirable, especially when serving sites out // of your control. Default: false + // TODO: is this really necessary per-policy? why not a global setting... ManageSync bool `json:"manage_sync,omitempty"` Issuer certmagic.Issuer `json:"-"` @@ -510,8 +544,7 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error { OnDemand: ond, Storage: storage, } - cfg := certmagic.New(tlsApp.certCache, template) - ap.magic = cfg + ap.magic = certmagic.New(tlsApp.certCache, template) if ap.IssuerRaw != nil { val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw") @@ -527,12 +560,12 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error { // ACME challenges -- it's an annoying, inelegant circular // dependency that I don't know how to resolve nicely!) if configger, ok := ap.Issuer.(ConfigSetter); ok { - configger.SetConfig(cfg) + configger.SetConfig(ap.magic) } - cfg.Issuer = ap.Issuer + ap.magic.Issuer = ap.Issuer if rev, ok := ap.Issuer.(certmagic.Revoker); ok { - cfg.Revoker = rev + ap.magic.Revoker = rev } return nil @@ -789,3 +822,10 @@ func (t *TLS) moveCertificates() error { return nil } + +// Interface guards +var ( + _ caddy.Provisioner = (*TLS)(nil) + _ caddy.Validator = (*TLS)(nil) + _ caddy.App = (*TLS)(nil) +) diff --git a/modules/standard/import.go b/modules/standard/import.go index 5ecfb4a..a88200f 100644 --- a/modules/standard/import.go +++ b/modules/standard/import.go @@ -6,6 +6,7 @@ import ( _ "github.com/caddyserver/caddy/v2/caddyconfig/json5" _ "github.com/caddyserver/caddy/v2/caddyconfig/jsonc" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" + _ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddytls" _ "github.com/caddyserver/caddy/v2/modules/caddytls/distributedstek" _ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek" -- cgit v1.2.3