From b8cba62643abf849411856bd92c42b59b98779f4 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 6 Mar 2020 23:15:25 -0700 Subject: Refactor for CertMagic v0.10; prepare for PKI app This is a breaking change primarily in two areas: - Storage paths for certificates have changed - Slight changes to JSON config parameters Huge improvements in this commit, to be detailed more in the release notes. The upcoming PKI app will be powered by Smallstep libraries. --- modules/caddytls/acmeissuer.go | 207 +++++++++++++++++ modules/caddytls/acmemanager.go | 252 --------------------- modules/caddytls/certselection.go | 2 +- modules/caddytls/connpolicy.go | 60 +++-- .../caddytls/distributedstek/distributedstek.go | 2 +- modules/caddytls/tls.go | 248 +++++++++++++++----- modules/caddytls/values.go | 41 +++- 7 files changed, 470 insertions(+), 342 deletions(-) create mode 100644 modules/caddytls/acmeissuer.go delete mode 100644 modules/caddytls/acmemanager.go (limited to 'modules/caddytls') diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go new file mode 100644 index 0000000..36fd76c --- /dev/null +++ b/modules/caddytls/acmeissuer.go @@ -0,0 +1,207 @@ +// 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 ( + "context" + "crypto/x509" + "fmt" + "io/ioutil" + "net/url" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" + "github.com/go-acme/lego/v3/challenge" +) + +func init() { + caddy.RegisterModule(ACMEIssuer{}) +} + +// ACMEIssuer makes an ACME manager +// for managing certificates using ACME. +// +// TODO: support multiple ACME endpoints (probably +// requires an array of these structs) - caddy would +// also have to load certs from the backup CAs if the +// first one is expired... +type ACMEIssuer struct { + // The URL to the CA's ACME directory endpoint. + CA string `json:"ca,omitempty"` + + // The URL to the test CA's ACME directory endpoint. + // This endpoint is only used during retries if there + // is a failure using the primary CA. + TestCA string `json:"test_ca,omitempty"` + + // Your email address, so the CA can contact you if necessary. + // Not required, but strongly recommended to provide one so + // you can be reached if there is a problem. Your email is + // not sent to any Caddy mothership or used for any purpose + // other than ACME transactions. + Email string `json:"email,omitempty"` + + // Time to wait before timing out an ACME operation. + ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"` + + // Configures the various ACME challenge types. + Challenges *ChallengesConfig `json:"challenges,omitempty"` + + // An array of files of CA certificates to accept when connecting to the + // ACME CA. Generally, you should only use this if the ACME CA endpoint + // is internal or for development/testing purposes. + TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"` + + rootPool *x509.CertPool + template certmagic.ACMEManager + magic *certmagic.Config +} + +// CaddyModule returns the Caddy module information. +func (ACMEIssuer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.issuance.acme", + New: func() caddy.Module { return new(ACMEIssuer) }, + } +} + +// Provision sets up m. +func (m *ACMEIssuer) Provision(ctx caddy.Context) error { + // DNS providers + if m.Challenges != nil && m.Challenges.DNSRaw != nil { + val, err := ctx.LoadModule(m.Challenges, "DNSRaw") + if err != nil { + return fmt.Errorf("loading DNS provider module: %v", err) + } + prov, err := val.(DNSProviderMaker).NewDNSProvider() + if err != nil { + return fmt.Errorf("making DNS provider: %v", err) + } + m.Challenges.DNS = prov + } + + // add any custom CAs to trust store + if len(m.TrustedRootsPEMFiles) > 0 { + m.rootPool = x509.NewCertPool() + for _, pemFile := range m.TrustedRootsPEMFiles { + pemData, err := ioutil.ReadFile(pemFile) + if err != nil { + return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err) + } + if !m.rootPool.AppendCertsFromPEM(pemData) { + return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err) + } + } + } + + m.template = m.makeIssuerTemplate() + + return nil +} + +func (m *ACMEIssuer) makeIssuerTemplate() certmagic.ACMEManager { + template := certmagic.ACMEManager{ + CA: m.CA, + Email: m.Email, + Agreed: true, + CertObtainTimeout: time.Duration(m.ACMETimeout), + TrustedRoots: m.rootPool, + } + + if m.Challenges != nil { + if m.Challenges.HTTP != nil { + template.DisableHTTPChallenge = m.Challenges.HTTP.Disabled + template.AltHTTPPort = m.Challenges.HTTP.AlternatePort + } + if m.Challenges.TLSALPN != nil { + template.DisableTLSALPNChallenge = m.Challenges.TLSALPN.Disabled + template.AltTLSALPNPort = m.Challenges.TLSALPN.AlternatePort + } + template.DNSProvider = m.Challenges.DNS + } + + return template +} + +// SetConfig sets the associated certmagic config for this issuer. +// This is required because ACME needs values from the config in +// order to solve the challenges during issuance. This implements +// the ConfigSetter interface. +func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) { + m.magic = cfg +} + +// PreCheck implements the certmagic.PreChecker interface. +func (m *ACMEIssuer) PreCheck(names []string, interactive bool) (skip bool, err error) { + return certmagic.NewACMEManager(m.magic, m.template).PreCheck(names, interactive) +} + +// Issue obtains a certificate for the given csr. +func (m *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + return certmagic.NewACMEManager(m.magic, m.template).Issue(ctx, csr) +} + +// IssuerKey returns the unique issuer key for the configured CA endpoint. +func (m *ACMEIssuer) IssuerKey() string { + return m.template.IssuerKey() // does not need storage and cache +} + +// Revoke revokes the given certificate. +func (m *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource) error { + return certmagic.NewACMEManager(m.magic, m.template).Revoke(ctx, cert) +} + +// onDemandAskRequest makes a request to the ask URL +// to see if a certificate can be obtained for name. +// The certificate request should be denied if this +// returns an error. +func onDemandAskRequest(ask string, name string) error { + askURL, err := url.Parse(ask) + if err != nil { + return fmt.Errorf("parsing ask URL: %v", err) + } + qs := askURL.Query() + qs.Set("domain", name) + askURL.RawQuery = qs.Encode() + + resp, err := onDemandAskClient.Get(askURL.String()) + if err != nil { + return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", + ask, name, err) + } + resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("certificate for hostname '%s' not allowed; non-2xx status code %d returned from %v", + name, resp.StatusCode, ask) + } + + return nil +} + +// DNSProviderMaker is a type that can create a new DNS provider. +// Modules in the tls.dns namespace should implement this interface. +type DNSProviderMaker interface { + NewDNSProvider() (challenge.Provider, error) +} + +// Interface guards +var ( + _ certmagic.Issuer = (*ACMEIssuer)(nil) + _ certmagic.Revoker = (*ACMEIssuer)(nil) + _ certmagic.PreChecker = (*ACMEIssuer)(nil) + _ ConfigSetter = (*ACMEIssuer)(nil) +) diff --git a/modules/caddytls/acmemanager.go b/modules/caddytls/acmemanager.go deleted file mode 100644 index df73545..0000000 --- a/modules/caddytls/acmemanager.go +++ /dev/null @@ -1,252 +0,0 @@ -// 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 ( - "crypto/x509" - "encoding/json" - "fmt" - "io/ioutil" - "net/url" - "time" - - "github.com/caddyserver/caddy/v2" - "github.com/go-acme/lego/v3/challenge" - "github.com/mholt/certmagic" -) - -func init() { - caddy.RegisterModule(ACMEManagerMaker{}) -} - -// ACMEManagerMaker makes an ACME manager -// for managing certificates using ACME. -// If crafting one manually rather than -// through the config-unmarshal process -// (provisioning), be sure to call -// SetDefaults to ensure sane defaults -// after you have configured this struct -// to your liking. -type ACMEManagerMaker struct { - // The URL to the CA's ACME directory endpoint. - CA string `json:"ca,omitempty"` - - // Your email address, so the CA can contact you if necessary. - // Not required, but strongly recommended to provide one so - // you can be reached if there is a problem. Your email is - // not sent to any Caddy mothership or used for any purpose - // other than ACME transactions. - Email string `json:"email,omitempty"` - - // How long before a certificate's expiration to try renewing it. - // Should usually be about 1/3 of certificate lifetime, but long - // enough to give yourself time to troubleshoot problems before - // expiration. Default: 30d - RenewAhead caddy.Duration `json:"renew_ahead,omitempty"` - - // The type of key to generate for the certificate. - // Supported values: `rsa2048`, `rsa4096`, `p256`, `p384`. - KeyType string `json:"key_type,omitempty"` - - // Time to wait before timing out an ACME operation. - ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"` - - // If true, certificates will be requested with MustStaple. Not all - // CAs support this, and there are potentially serious consequences - // of enabling this feature without proper threat modeling. - MustStaple bool `json:"must_staple,omitempty"` - - // Configures the various ACME challenge types. - Challenges *ChallengesConfig `json:"challenges,omitempty"` - - // If true, certificates will be managed "on demand", that is, during - // TLS handshakes or when needed, as opposed to at startup or config - // load. - OnDemand bool `json:"on_demand,omitempty"` - - // Optionally configure a separate storage module associated with this - // manager, instead of using Caddy's global/default-configured storage. - Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` - - // An array of files of CA certificates to accept when connecting to the - // ACME CA. Generally, you should only use this if the ACME CA endpoint - // is internal or for development/testing purposes. - TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"` - - storage certmagic.Storage - rootPool *x509.CertPool -} - -// CaddyModule returns the Caddy module information. -func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "tls.management.acme", - New: func() caddy.Module { return new(ACMEManagerMaker) }, - } -} - -// NewManager is a no-op to satisfy the ManagerMaker interface, -// because this manager type is a special case. -func (m ACMEManagerMaker) NewManager(interactive bool) (certmagic.Manager, error) { - return nil, nil -} - -// Provision sets up m. -func (m *ACMEManagerMaker) Provision(ctx caddy.Context) error { - // DNS providers - if m.Challenges != nil && m.Challenges.DNSRaw != nil { - val, err := ctx.LoadModule(m.Challenges, "DNSRaw") - if err != nil { - return fmt.Errorf("loading DNS provider module: %v", err) - } - prov, err := val.(DNSProviderMaker).NewDNSProvider() - if err != nil { - return fmt.Errorf("making DNS provider: %v", err) - } - m.Challenges.DNS = prov - } - - // policy-specific storage implementation - if m.Storage != nil { - val, err := ctx.LoadModule(m, "Storage") - if err != nil { - return fmt.Errorf("loading TLS storage module: %v", err) - } - cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() - if err != nil { - return fmt.Errorf("creating TLS storage configuration: %v", err) - } - m.storage = cmStorage - } - - // add any custom CAs to trust store - if len(m.TrustedRootsPEMFiles) > 0 { - m.rootPool = x509.NewCertPool() - for _, pemFile := range m.TrustedRootsPEMFiles { - pemData, err := ioutil.ReadFile(pemFile) - if err != nil { - return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err) - } - if !m.rootPool.AppendCertsFromPEM(pemData) { - return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err) - } - } - } - - return nil -} - -// makeCertMagicConfig converts m into a certmagic.Config, because -// this is a special case where the default manager is the certmagic -// Config and not a separate manager. -func (m *ACMEManagerMaker) makeCertMagicConfig(ctx caddy.Context) certmagic.Config { - storage := m.storage - if storage == nil { - storage = ctx.Storage() - } - - var ond *certmagic.OnDemandConfig - if m.OnDemand { - var onDemand *OnDemandConfig - appVal, err := ctx.App("tls") - if err == nil && appVal.(*TLS).Automation != nil { - onDemand = appVal.(*TLS).Automation.OnDemand - } - - ond = &certmagic.OnDemandConfig{ - DecisionFunc: func(name string) error { - if onDemand != nil { - if onDemand.Ask != "" { - err := onDemandAskRequest(onDemand.Ask, name) - if err != nil { - return err - } - } - // check the rate limiter last because - // doing so makes a reservation - if !onDemandRateLimiter.Allow() { - return fmt.Errorf("on-demand rate limit exceeded") - } - } - return nil - }, - } - } - - cfg := certmagic.Config{ - CA: m.CA, - Email: m.Email, - Agreed: true, - RenewDurationBefore: time.Duration(m.RenewAhead), - KeyType: supportedCertKeyTypes[m.KeyType], - CertObtainTimeout: time.Duration(m.ACMETimeout), - OnDemand: ond, - MustStaple: m.MustStaple, - Storage: storage, - TrustedRoots: m.rootPool, - // TODO: listenHost - } - - if m.Challenges != nil { - if m.Challenges.HTTP != nil { - cfg.DisableHTTPChallenge = m.Challenges.HTTP.Disabled - cfg.AltHTTPPort = m.Challenges.HTTP.AlternatePort - } - if m.Challenges.TLSALPN != nil { - cfg.DisableTLSALPNChallenge = m.Challenges.TLSALPN.Disabled - cfg.AltTLSALPNPort = m.Challenges.TLSALPN.AlternatePort - } - cfg.DNSProvider = m.Challenges.DNS - } - - return cfg -} - -// onDemandAskRequest makes a request to the ask URL -// to see if a certificate can be obtained for name. -// The certificate request should be denied if this -// returns an error. -func onDemandAskRequest(ask string, name string) error { - askURL, err := url.Parse(ask) - if err != nil { - return fmt.Errorf("parsing ask URL: %v", err) - } - qs := askURL.Query() - qs.Set("domain", name) - askURL.RawQuery = qs.Encode() - - resp, err := onDemandAskClient.Get(askURL.String()) - if err != nil { - return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", - ask, name, err) - } - resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("certificate for hostname '%s' not allowed; non-2xx status code %d returned from %v", - name, resp.StatusCode, ask) - } - - return nil -} - -// DNSProviderMaker is a type that can create a new DNS provider. -// Modules in the tls.dns namespace should implement this interface. -type DNSProviderMaker interface { - NewDNSProvider() (challenge.Provider, error) -} - -// Interface guard -var _ ManagerMaker = (*ACMEManagerMaker)(nil) diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go index 0d49eb7..343c740 100644 --- a/modules/caddytls/certselection.go +++ b/modules/caddytls/certselection.go @@ -7,7 +7,7 @@ import ( "math/big" "github.com/caddyserver/caddy/v2" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" ) func init() { diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index cdc9b9d..9c61c72 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -23,8 +23,8 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" "github.com/go-acme/lego/v3/challenge/tlsalpn01" - "github.com/mholt/certmagic" ) // ConnectionPolicies is an ordered group of connection policies; @@ -32,16 +32,15 @@ import ( // connections at handshake-time. type ConnectionPolicies []*ConnectionPolicy -// TLSConfig converts the group of policies to a standard-lib-compatible -// TLS configuration which selects the first matching policy based on -// the ClientHello. -func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) { - // set up each of the connection policies +// Provision sets up each connection policy. It should be called +// during the Validate() phase, after the TLS app (if any) is +// already set up. +func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { for i, pol := range cp { // matchers mods, err := ctx.LoadModule(pol, "MatchersRaw") if err != nil { - return nil, fmt.Errorf("loading handshake matchers: %v", err) + return fmt.Errorf("loading handshake matchers: %v", err) } for _, modIface := range mods.(map[string]interface{}) { cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher)) @@ -51,20 +50,24 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) { if pol.CertSelection != nil { val, err := ctx.LoadModule(pol, "CertSelection") if err != nil { - return nil, fmt.Errorf("loading certificate selection module: %s", err) + return fmt.Errorf("loading certificate selection module: %s", err) } cp[i].certSelector = val.(certmagic.CertificateSelector) } - } - // pre-build standard TLS configs so we don't have to at handshake-time - for i := range cp { - err := cp[i].buildStandardTLSConfig(ctx) + // pre-build standard TLS config so we don't have to at handshake-time + err = pol.buildStandardTLSConfig(ctx) if err != nil { - return nil, fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) + return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) } } + return nil +} + +// TLSConfig returns a standard-lib-compatible TLS configuration which +// selects the first matching policy based on the ClientHello. +func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // using ServerName to match policies is extremely common, especially in configs // with lots and lots of different policies; we can fast-track those by indexing // them by SNI, so we don't have to iterate potentially thousands of policies @@ -102,7 +105,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) { return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) }, - }, nil + } } // ConnectionPolicy specifies the logic for handling a TLS handshake. @@ -137,6 +140,10 @@ type ConnectionPolicy struct { // Enables and configures TLS client authentication. ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"` + // DefaultSNI becomes the ServerName in a ClientHello if there + // is no policy configured for the empty SNI value. + DefaultSNI string `json:"default_sni,omitempty"` + matchers []ConnectionMatcher certSelector certmagic.CertificateSelector @@ -158,15 +165,24 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { NextProtos: p.ALPN, PreferServerCipherSuites: true, GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - cfgTpl, err := tlsApp.getConfigForName(hello.ServerName) - if err != nil { - return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err) - } - newCfg := certmagic.New(tlsApp.certCache, cfgTpl) + // TODO: I don't love how this works: we pre-build certmagic configs + // so that handshakes are faster. Unfortunately, certmagic configs are + // comprised of settings from both a TLS connection policy and a TLS + // automation policy. The only two fields (as of March 2020; v2 beta 16) + // of a certmagic config that come from the TLS connection policy are + // CertSelection and DefaultServerName, so an automation policy is what + // builds the base certmagic config. Since the pre-built config is + // shared, I don't think we can change any of its fields per-handshake, + // hence the awkward shallow copy (dereference) here and the subsequent + // changing of some of its fields. I'm worried this dereference allocates + // more at handshake-time, but I don't know how to practically pre-build + // a certmagic config for each combination of conn policy + automation policy... + cfg := *tlsApp.getConfigForName(hello.ServerName) if p.certSelector != nil { - newCfg.CertSelection = p.certSelector + cfg.CertSelection = p.certSelector } - return newCfg.GetCertificate(hello) + cfg.DefaultServerName = p.DefaultSNI + return cfg.GetCertificate(hello) }, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, @@ -240,8 +256,6 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { } } - // TODO: other fields - setDefaultTLSParams(cfg) p.stdTLSConfig = cfg diff --git a/modules/caddytls/distributedstek/distributedstek.go b/modules/caddytls/distributedstek/distributedstek.go index cef3733..6fc48a2 100644 --- a/modules/caddytls/distributedstek/distributedstek.go +++ b/modules/caddytls/distributedstek/distributedstek.go @@ -32,7 +32,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" ) func init() { diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 6be480a..a490ffe 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -23,8 +23,8 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" "github.com/go-acme/lego/v3/challenge" - "github.com/mholt/certmagic" "go.uber.org/zap" ) @@ -71,13 +71,15 @@ func (TLS) CaddyModule() caddy.ModuleInfo { // Provision sets up the configuration for the TLS app. func (t *TLS) Provision(ctx caddy.Context) error { + // TODO: Move assets to the new folder structure!! + t.ctx = ctx t.logger = ctx.Logger(t) // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ - GetConfigForCert: func(cert certmagic.Certificate) (certmagic.Config, error) { - return t.getConfigForName(cert.Names[0]) + GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { + return t.getConfigForName(cert.Names[0]), nil }, } if t.Automation != nil { @@ -87,20 +89,25 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.certCache = certmagic.NewCache(cacheOpts) // automation/management policies - if t.Automation != nil { - for i, ap := range t.Automation.Policies { - val, err := ctx.LoadModule(ap, "ManagementRaw") - if err != nil { - return fmt.Errorf("loading TLS automation management module: %s", err) - } - t.Automation.Policies[i].Management = val.(ManagerMaker) + if t.Automation == nil { + t.Automation = new(AutomationConfig) + } + t.Automation.defaultAutomationPolicy = new(AutomationPolicy) + err := t.Automation.defaultAutomationPolicy.provision(t) + if err != nil { + return fmt.Errorf("provisioning default automation policy: %v", err) + } + for i, ap := range t.Automation.Policies { + err := ap.provision(t) + if err != nil { + return fmt.Errorf("provisioning automation policy %d: %v", i, err) } } // certificate loaders val, err := ctx.LoadModule(t, "CertificatesRaw") if err != nil { - return fmt.Errorf("loading TLS automation management module: %s", err) + return fmt.Errorf("loading certificate loader modules: %s", err) } for modName, modIface := range val.(map[string]interface{}) { if modName == "automate" { @@ -216,12 +223,11 @@ func (t *TLS) Manage(names []string) error { // certmagic.Config for each (potentially large) group of names // and call ManageSync/ManageAsync just once for the whole batch for ap, names := range policyToNames { - magic := certmagic.New(t.certCache, ap.makeCertMagicConfig(t.ctx)) var err error if ap.ManageSync { - err = magic.ManageSync(names) + err = ap.magic.ManageSync(names) } else { - err = magic.ManageAsync(t.ctx.Context, names) + err = ap.magic.ManageAsync(t.ctx.Context, names) } if err != nil { return fmt.Errorf("automate: manage %v: %v", names, err) @@ -232,36 +238,54 @@ func (t *TLS) Manage(names []string) error { } // HandleHTTPChallenge ensures that the HTTP challenge is handled for the -// certificate named by r.Host, if it is an HTTP challenge request. +// certificate named by r.Host, if it is an HTTP challenge request. It +// requires that the automation policy for r.Host has an issue of type +// *certmagic.ACMEManager. func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { if !certmagic.LooksLikeHTTPChallenge(r) { return false } ap := t.getAutomationPolicyForName(r.Host) - magic := certmagic.New(t.certCache, ap.makeCertMagicConfig(t.ctx)) - return magic.HandleHTTPChallenge(w, r) + if ap.magic.Issuer == nil { + return false + } + if am, ok := ap.magic.Issuer.(*certmagic.ACMEManager); ok { + return am.HandleHTTPChallenge(w, r) + } + return false +} + +// AddAutomationPolicy provisions and adds ap to the list of the app's +// automation policies. +func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { + if t.Automation == nil { + t.Automation = new(AutomationConfig) + } + err := ap.provision(t) + if err != nil { + return err + } + t.Automation.Policies = append(t.Automation.Policies, ap) + return nil } -func (t *TLS) getConfigForName(name string) (certmagic.Config, error) { +func (t *TLS) getConfigForName(name string) *certmagic.Config { ap := t.getAutomationPolicyForName(name) - return ap.makeCertMagicConfig(t.ctx), nil + return ap.magic } func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy { - if t.Automation != nil { - for _, ap := range t.Automation.Policies { - if len(ap.Hosts) == 0 { - // no host filter is an automatic match + for _, ap := range t.Automation.Policies { + if len(ap.Hosts) == 0 { + return ap // no host filter is an automatic match + } + for _, h := range ap.Hosts { + if h == name { return ap } - for _, h := range ap.Hosts { - if h == name { - return ap - } - } } } - return defaultAutomationPolicy + return t.Automation.defaultAutomationPolicy } // AllMatchingCertificates returns the list of all certificates in @@ -309,10 +333,8 @@ func (t *TLS) cleanStorageUnits() { // then clean each storage defined in ACME automation policies if t.Automation != nil { for _, ap := range t.Automation.Policies { - if acmeMgmt, ok := ap.Management.(ACMEManagerMaker); ok { - if acmeMgmt.storage != nil { - certmagic.CleanStorage(acmeMgmt.storage, options) - } + if ap.storage != nil { + certmagic.CleanStorage(ap.storage, options) } } } @@ -355,23 +377,56 @@ type AutomationConfig struct { OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"` // Every so often, Caddy will scan all loaded, managed - // certificates for expiration. Certificates which are - // about 2/3 into their valid lifetime are due for - // renewal. This setting changes how frequently the scan - // is performed. If your certificate lifetimes are very - // short (less than ~1 week), you should customize this. + // certificates for expiration. This setting changes how + // frequently the scan for expiring certificates is + // performed. If your certificate lifetimes are very + // short (less than ~24 hours), you should set this to + // a low value. RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"` + + defaultAutomationPolicy *AutomationPolicy } // AutomationPolicy designates the policy for automating the // management (obtaining, renewal, and revocation) of managed // TLS certificates. +// +// An AutomationPolicy value is not valid until it has been +// provisioned; use the `AddAutomationPolicy()` method on the +// TLS app to properly provision a new policy. type AutomationPolicy struct { // Which hostnames this policy applies to. Hosts []string `json:"hosts,omitempty"` - // How to manage certificates. - ManagementRaw json.RawMessage `json:"management,omitempty" caddy:"namespace=tls.management inline_key=module"` + // The module that will issue certificates. Default: acme + IssuerRaw json.RawMessage `json:"issuer,omitempty" caddy:"namespace=tls.issuance inline_key=module"` + + // If true, certificates will be requested with MustStaple. Not all + // CAs support this, and there are potentially serious consequences + // of enabling this feature without proper threat modeling. + MustStaple bool `json:"must_staple,omitempty"` + + // How long before a certificate's expiration to try renewing it, + // as a function of its total lifetime. As a general and conservative + // rule, it is a good idea to renew a certificate when it has about + // 1/3 of its total lifetime remaining. This utilizes the majority + // of the certificate's lifetime while still saving time to + // troubleshoot problems. However, for extremely short-lived certs, + // you may wish to increase the ratio to ~1/2. + RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"` + + // The type of key to generate for certificates. + // Supported values: `ed25519`, `p256`, `p384`, `rsa2048`, `rsa4096`. + KeyType string `json:"key_type,omitempty"` + + // Optionally configure a separate storage module associated with this + // manager, instead of using Caddy's global/default-configured storage. + StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` + + // If true, certificates will be managed "on demand", that is, during + // TLS handshakes or when needed, as opposed to at startup or config + // load. + OnDemand bool `json:"on_demand,omitempty"` // If true, certificate management will be conducted // in the foreground; this will block config reloads @@ -381,23 +436,96 @@ type AutomationPolicy struct { // of your control. Default: false ManageSync bool `json:"manage_sync,omitempty"` - Management ManagerMaker `json:"-"` + Issuer certmagic.Issuer `json:"-"` + + magic *certmagic.Config + storage certmagic.Storage } -// makeCertMagicConfig converts ap into a CertMagic config. Passing onDemand -// is necessary because the automation policy does not have convenient access -// to the TLS app's global on-demand policies; -func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Config { - // default manager (ACME) is a special case because of how CertMagic is designed - // TODO: refactor certmagic so that ACME manager is not a special case by extracting - // its config fields out of the certmagic.Config struct, or something... - if acmeMgmt, ok := ap.Management.(*ACMEManagerMaker); ok { - return acmeMgmt.makeCertMagicConfig(ctx) +// provision converts ap into a CertMagic config. +func (ap *AutomationPolicy) provision(tlsApp *TLS) error { + // policy-specific storage implementation + if ap.StorageRaw != nil { + val, err := tlsApp.ctx.LoadModule(ap, "StorageRaw") + if err != nil { + return fmt.Errorf("loading TLS storage module: %v", err) + } + cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() + if err != nil { + return fmt.Errorf("creating TLS storage configuration: %v", err) + } + ap.storage = cmStorage + } + + var ond *certmagic.OnDemandConfig + if ap.OnDemand { + var onDemand *OnDemandConfig + if tlsApp.Automation != nil { + onDemand = tlsApp.Automation.OnDemand + } + + ond = &certmagic.OnDemandConfig{ + DecisionFunc: func(name string) error { + if onDemand != nil { + if onDemand.Ask != "" { + err := onDemandAskRequest(onDemand.Ask, name) + if err != nil { + return err + } + } + // check the rate limiter last because + // doing so makes a reservation + if !onDemandRateLimiter.Allow() { + return fmt.Errorf("on-demand rate limit exceeded") + } + } + return nil + }, + } + } + + keySource := certmagic.StandardKeyGenerator{ + KeyType: supportedCertKeyTypes[ap.KeyType], + } + + storage := ap.storage + if storage == nil { + storage = tlsApp.ctx.Storage() } - return certmagic.Config{ - NewManager: ap.Management.NewManager, + template := certmagic.Config{ + MustStaple: ap.MustStaple, + RenewalWindowRatio: ap.RenewalWindowRatio, + KeySource: keySource, + OnDemand: ond, + Storage: storage, } + cfg := certmagic.New(tlsApp.certCache, template) + ap.magic = cfg + + if ap.IssuerRaw != nil { + val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw") + if err != nil { + return fmt.Errorf("loading TLS automation management module: %s", err) + } + ap.Issuer = val.(certmagic.Issuer) + } + + // sometimes issuers may need the parent certmagic.Config in + // order to function properly (for example, ACMEIssuer needs + // access to the correct storage and cache so it can solve + // 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) + } + + cfg.Issuer = ap.Issuer + if rev, ok := ap.Issuer.(certmagic.Revoker); ok { + cfg.Revoker = rev + } + + return nil } // ChallengesConfig configures the ACME challenges. @@ -482,11 +610,6 @@ type RateLimit struct { Burst int `json:"burst,omitempty"` } -// ManagerMaker makes a certificate manager. -type ManagerMaker interface { - NewManager(interactive bool) (certmagic.Manager, error) -} - // AutomateLoader is a no-op certificate loader module // that is treated as a special case: it uses this app's // automation features to load certificates for the @@ -502,6 +625,15 @@ func (AutomateLoader) CaddyModule() caddy.ModuleInfo { } } +// ConfigSetter is implemented by certmagic.Issuers that +// need access to a parent certmagic.Config as part of +// their provisioning phase. For example, the ACMEIssuer +// requires a config so it can access storage and the +// cache to solve ACME challenges. +type ConfigSetter interface { + SetConfig(cfg *certmagic.Config) +} + // These perpetual values are used for on-demand TLS. var ( onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) @@ -521,8 +653,6 @@ var ( storageCleanMu sync.Mutex ) -var defaultAutomationPolicy = &AutomationPolicy{Management: new(ACMEManagerMaker)} - // Interface guards var ( _ caddy.App = (*TLS)(nil) diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 21a6b33..40b0de0 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -17,8 +17,9 @@ package caddytls import ( "crypto/tls" "crypto/x509" + "fmt" - "github.com/go-acme/lego/v3/certcrypto" + "github.com/caddyserver/certmagic" "github.com/klauspost/cpuid" ) @@ -101,11 +102,12 @@ var SupportedCurves = map[string]tls.CurveID{ // supportedCertKeyTypes is all the key types that are supported // for certificates that are obtained through ACME. -var supportedCertKeyTypes = map[string]certcrypto.KeyType{ - "rsa_2048": certcrypto.RSA2048, - "rsa_4096": certcrypto.RSA4096, - "ec_p256": certcrypto.EC256, - "ec_p384": certcrypto.EC384, +var supportedCertKeyTypes = map[string]certmagic.KeyType{ + "rsa2048": certmagic.RSA2048, + "rsa4096": certmagic.RSA4096, + "p256": certmagic.P256, + "p384": certmagic.P384, + "ed25519": certmagic.ED25519, } // defaultCurves is the list of only the curves we want to use @@ -127,9 +129,36 @@ var SupportedProtocols = map[string]uint16{ "tls1.3": tls.VersionTLS13, } +// unsupportedProtocols is a map of unsupported protocols. +// Used for logging only, not enforcement. +var unsupportedProtocols = map[string]uint16{ + "ssl3.0": tls.VersionSSL30, + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, +} + // publicKeyAlgorithms is the map of supported public key algorithms. var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{ "rsa": x509.RSA, "dsa": x509.DSA, "ecdsa": x509.ECDSA, } + +// ProtocolName returns the standard name for the passed protocol version ID +// (e.g. "TLS1.3") or a fallback representation of the ID value if the version +// is not supported. +func ProtocolName(id uint16) string { + for k, v := range SupportedProtocols { + if v == id { + return k + } + } + + for k, v := range unsupportedProtocols { + if v == id { + return k + } + } + + return fmt.Sprintf("0x%04x", id) +} -- cgit v1.2.3