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