diff options
author | Matthew Holt <mholt@users.noreply.github.com> | 2020-03-06 23:15:25 -0700 |
---|---|---|
committer | Matthew Holt <mholt@users.noreply.github.com> | 2020-03-06 23:15:25 -0700 |
commit | b8cba62643abf849411856bd92c42b59b98779f4 (patch) | |
tree | 518ddc4db0ce065353fd6f499c8eaf2975b65d13 /modules/caddytls | |
parent | 7cca291d62c910c0544f0c0169a8f0c81627e5d3 (diff) |
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.
Diffstat (limited to 'modules/caddytls')
-rw-r--r-- | modules/caddytls/acmeissuer.go | 207 | ||||
-rw-r--r-- | modules/caddytls/acmemanager.go | 252 | ||||
-rw-r--r-- | modules/caddytls/certselection.go | 2 | ||||
-rw-r--r-- | modules/caddytls/connpolicy.go | 60 | ||||
-rw-r--r-- | modules/caddytls/distributedstek/distributedstek.go | 2 | ||||
-rw-r--r-- | modules/caddytls/tls.go | 248 | ||||
-rw-r--r-- | modules/caddytls/values.go | 41 |
7 files changed, 470 insertions, 342 deletions
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) +} |