Matthew Holt
5 years ago
26 changed files with 1161 additions and 540 deletions
File diff suppressed because it is too large
@ -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) |
||||
|
) |
@ -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 determine 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) |
|
Loading…
Reference in new issue