mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
09188981c4
* tls: Add support for the tls-alpn-01 challenge Also updates lego/acme to latest on master. TODO: This implementation of the tls-alpn challenge is not yet solvable in a distributed Caddy cluster like the http challenge is. * build: Allow building with the race detector * tls: Support distributed solving of the TLS-ALPN-01 challenge * Update vendor and add a todo in MITM checker
311 lines
11 KiB
Go
311 lines
11 KiB
Go
// Copyright 2015 Light Code Labs, LLC
|
|
//
|
|
// 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 facilitates the management of TLS assets and integrates
|
|
// Let's Encrypt functionality into Caddy with first-class support for
|
|
// creating and renewing certificates automatically. It also implements
|
|
// the tls directive.
|
|
//
|
|
// This package is meant to be used by Caddy server types. To use the
|
|
// tls directive, a server type must import this package and call
|
|
// RegisterConfigGetter(). The server type must make and keep track of
|
|
// the caddytls.Config structs that this package produces. It must also
|
|
// add tls to its list of directives. When it comes time to make the
|
|
// server instances, the server type can call MakeTLSConfig() to convert
|
|
// a []caddytls.Config to a single tls.Config for use in tls.NewListener().
|
|
// It is also recommended to call RotateSessionTicketKeys() when
|
|
// starting a new listener.
|
|
package caddytls
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/mholt/caddy"
|
|
"github.com/xenolf/lego/acme"
|
|
)
|
|
|
|
// HostQualifies returns true if the hostname alone
|
|
// appears eligible for automatic HTTPS. For example:
|
|
// localhost, empty hostname, and IP addresses are
|
|
// not eligible because we cannot obtain certificates
|
|
// for those names. Wildcard names are allowed, as long
|
|
// as they conform to CABF requirements (only one wildcard
|
|
// label, and it must be the left-most label).
|
|
func HostQualifies(hostname string) bool {
|
|
return hostname != "localhost" && // localhost is ineligible
|
|
|
|
// hostname must not be empty
|
|
strings.TrimSpace(hostname) != "" &&
|
|
|
|
// only one wildcard label allowed, and it must be left-most
|
|
(!strings.Contains(hostname, "*") ||
|
|
(strings.Count(hostname, "*") == 1 &&
|
|
strings.HasPrefix(hostname, "*."))) &&
|
|
|
|
// must not start or end with a dot
|
|
!strings.HasPrefix(hostname, ".") &&
|
|
!strings.HasSuffix(hostname, ".") &&
|
|
|
|
// cannot be an IP address, see
|
|
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
|
|
net.ParseIP(hostname) == nil
|
|
}
|
|
|
|
// saveCertResource saves the certificate resource to disk. This
|
|
// includes the certificate file itself, the private key, and the
|
|
// metadata file.
|
|
func saveCertResource(storage Storage, cert *acme.CertificateResource) error {
|
|
// Save cert, private key, and metadata
|
|
siteData := &SiteData{
|
|
Cert: cert.Certificate,
|
|
Key: cert.PrivateKey,
|
|
}
|
|
var err error
|
|
siteData.Meta, err = json.MarshalIndent(&cert, "", "\t")
|
|
if err == nil {
|
|
err = storage.StoreSite(cert.Domain, siteData)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Revoke revokes the certificate for host via ACME protocol.
|
|
// It assumes the certificate was obtained from the
|
|
// CA at DefaultCAUrl.
|
|
func Revoke(host string) error {
|
|
client, err := newACMEClient(new(Config), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.Revoke(host)
|
|
}
|
|
|
|
// tlsALPNSolver is a type that can solve TLS-ALPN challenges using
|
|
// an existing listener and our custom, in-memory certificate cache.
|
|
type tlsALPNSolver struct {
|
|
certCache *certificateCache
|
|
}
|
|
|
|
// Present adds the challenge certificate to the cache.
|
|
func (s tlsALPNSolver) Present(domain, token, keyAuth string) error {
|
|
cert, err := acme.TLSALPNChallengeCert(domain, keyAuth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
certHash := hashCertificateChain(cert.Certificate)
|
|
s.certCache.Lock()
|
|
s.certCache.cache[tlsALPNCertKeyName(domain)] = Certificate{
|
|
Certificate: *cert,
|
|
Names: []string{domain},
|
|
Hash: certHash, // perhaps not necesssary
|
|
}
|
|
s.certCache.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// CleanUp removes the challenge certificate from the cache.
|
|
func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error {
|
|
s.certCache.Lock()
|
|
delete(s.certCache.cache, domain)
|
|
s.certCache.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// tlsALPNCertKeyName returns the key to use when caching a cert
|
|
// for use with the TLS-ALPN ACME challenge. It is simply to help
|
|
// avoid conflicts (although at time of writing, there shouldn't
|
|
// be, since the cert cache is keyed by hash of certificate chain).
|
|
func tlsALPNCertKeyName(sniName string) string {
|
|
return sniName + ":acme-tls-alpn"
|
|
}
|
|
|
|
// distributedSolver allows the ACME HTTP-01 and TLS-ALPN challenges
|
|
// to be solved by an instance other than the one which initiated it.
|
|
// This is useful behind load balancers or in other cluster/fleet
|
|
// configurations. The only requirement is that this (the initiating)
|
|
// instance share the $CADDYPATH/acme folder with the instance that
|
|
// will complete the challenge. Mounting the folder locally should be
|
|
// sufficient.
|
|
//
|
|
// Obviously, the instance which completes the challenge must be
|
|
// serving on the HTTPChallengePort for the HTTP-01 challenge or the
|
|
// TLSALPNChallengePort for the TLS-ALPN-01 challenge (or have all
|
|
// the packets port-forwarded) to receive and handle the request. The
|
|
// server which receives the challenge must handle it by checking to
|
|
// see if a file exists, e.g.:
|
|
// $CADDYPATH/acme/challenge_tokens/example.com.json
|
|
// and if so, decode it and use it to serve up the correct response.
|
|
// Caddy's HTTP server does this by default (for HTTP-01) and so does
|
|
// its TLS package (for TLS-ALPN-01).
|
|
//
|
|
// So as long as the folder is shared, this will just work. There are
|
|
// no other requirements. The instances may be on other machines or
|
|
// even other networks, as long as they share the folder as part of
|
|
// the local file system.
|
|
//
|
|
// This solver works by persisting the token and keyauth information
|
|
// to disk in the shared folder when the authorization is presented,
|
|
// and then deletes it when it is cleaned up.
|
|
type distributedSolver struct {
|
|
// As the distributedSolver is only a wrapper over the actual
|
|
// solver, place the actual solver here
|
|
providerServer ChallengeProvider
|
|
}
|
|
|
|
// Present adds the challenge certificate to the cache.
|
|
func (dhs distributedSolver) Present(domain, token, keyAuth string) error {
|
|
if dhs.providerServer != nil {
|
|
err := dhs.providerServer.Present(domain, token, keyAuth)
|
|
if err != nil {
|
|
return fmt.Errorf("presenting with standard provider server: %v", err)
|
|
}
|
|
}
|
|
|
|
err := os.MkdirAll(dhs.challengeTokensBasePath(), 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
infoBytes, err := json.Marshal(challengeInfo{
|
|
Domain: domain,
|
|
Token: token,
|
|
KeyAuth: keyAuth,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ioutil.WriteFile(dhs.challengeTokensPath(domain), infoBytes, 0644)
|
|
}
|
|
|
|
// CleanUp removes the challenge certificate from the cache.
|
|
func (dhs distributedSolver) CleanUp(domain, token, keyAuth string) error {
|
|
if dhs.providerServer != nil {
|
|
err := dhs.providerServer.CleanUp(domain, token, keyAuth)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Cleaning up standard provider server: %v", err)
|
|
}
|
|
}
|
|
return os.Remove(dhs.challengeTokensPath(domain))
|
|
}
|
|
|
|
func (dhs distributedSolver) challengeTokensPath(domain string) string {
|
|
domainFile := fileSafe(domain)
|
|
return filepath.Join(dhs.challengeTokensBasePath(), domainFile+".json")
|
|
}
|
|
|
|
func (dhs distributedSolver) challengeTokensBasePath() string {
|
|
return filepath.Join(caddy.AssetsPath(), "acme", "challenge_tokens")
|
|
}
|
|
|
|
type challengeInfo struct {
|
|
Domain, Token, KeyAuth string
|
|
}
|
|
|
|
// ConfigHolder is any type that has a Config; it presumably is
|
|
// connected to a hostname and port on which it is serving.
|
|
type ConfigHolder interface {
|
|
TLSConfig() *Config
|
|
Host() string
|
|
Port() string
|
|
}
|
|
|
|
// QualifiesForManagedTLS returns true if c qualifies for
|
|
// for managed TLS (but not on-demand TLS specifically).
|
|
// It does NOT check to see if a cert and key already exist
|
|
// for the config. If the return value is true, you should
|
|
// be OK to set c.TLSConfig().Managed to true; then you should
|
|
// check that value in the future instead, because the process
|
|
// of setting up the config may make it look like it doesn't
|
|
// qualify even though it originally did.
|
|
func QualifiesForManagedTLS(c ConfigHolder) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
tlsConfig := c.TLSConfig()
|
|
if tlsConfig == nil {
|
|
return false
|
|
}
|
|
|
|
return (!tlsConfig.Manual || tlsConfig.OnDemand) && // user might provide own cert and key
|
|
|
|
// if self-signed, we've already generated one to use
|
|
!tlsConfig.SelfSigned &&
|
|
|
|
// user can force-disable managed TLS
|
|
c.Port() != "80" &&
|
|
tlsConfig.ACMEEmail != "off" &&
|
|
|
|
// we get can't certs for some kinds of hostnames, but
|
|
// on-demand TLS allows empty hostnames at startup
|
|
(HostQualifies(c.Host()) || tlsConfig.OnDemand)
|
|
}
|
|
|
|
// ChallengeProvider defines an own type that should be used in Caddy plugins
|
|
// over acme.ChallengeProvider. Using acme.ChallengeProvider causes version mismatches
|
|
// with vendored dependencies (see https://github.com/mattfarina/golang-broken-vendor)
|
|
//
|
|
// acme.ChallengeProvider is an interface that allows the implementation of custom
|
|
// challenge providers. For more details, see:
|
|
// https://godoc.org/github.com/xenolf/lego/acme#ChallengeProvider
|
|
type ChallengeProvider acme.ChallengeProvider
|
|
|
|
// DNSProviderConstructor is a function that takes credentials and
|
|
// returns a type that can solve the ACME DNS challenges.
|
|
type DNSProviderConstructor func(credentials ...string) (ChallengeProvider, error)
|
|
|
|
// dnsProviders is the list of DNS providers that have been plugged in.
|
|
var dnsProviders = make(map[string]DNSProviderConstructor)
|
|
|
|
// RegisterDNSProvider registers provider by name for solving the ACME DNS challenge.
|
|
func RegisterDNSProvider(name string, provider DNSProviderConstructor) {
|
|
dnsProviders[name] = provider
|
|
caddy.RegisterPlugin("tls.dns."+name, caddy.Plugin{})
|
|
}
|
|
|
|
var (
|
|
// DefaultEmail represents the Let's Encrypt account email to use if none provided.
|
|
DefaultEmail string
|
|
|
|
// Agreed indicates whether user has agreed to the Let's Encrypt SA.
|
|
Agreed bool
|
|
|
|
// DefaultCAUrl is the default URL to the CA's ACME directory endpoint.
|
|
// It's very important to set this unless you set it in every Config.
|
|
DefaultCAUrl string
|
|
|
|
// DefaultKeyType is used as the type of key for new certificates
|
|
// when no other key type is specified.
|
|
DefaultKeyType = acme.RSA2048
|
|
|
|
// DisableHTTPChallenge will disable all HTTP challenges.
|
|
DisableHTTPChallenge bool
|
|
|
|
// DisableTLSALPNChallenge will disable all TLS-ALPN challenges.
|
|
DisableTLSALPNChallenge bool
|
|
)
|
|
|
|
var storageProviders = make(map[string]StorageConstructor)
|
|
|
|
// RegisterStorageProvider registers provider by name for storing tls data
|
|
func RegisterStorageProvider(name string, provider StorageConstructor) {
|
|
storageProviders[name] = provider
|
|
caddy.RegisterPlugin("tls.storage."+name, caddy.Plugin{})
|
|
}
|