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