mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-26 02:09:47 +08:00
e3f2d96a5e
This commit removes _almost_ all instances of hard-coded ports 80 and 443 strings, and now allows the user to define what the HTTP and HTTPS ports are by the -http-port and -https-ports flags. (One instance of "80" is still hard-coded in tls.go because it cannot import httpserver to get access to the HTTP port variable. I don't suspect this will be a problem in practice, but one workaround would be to define an exported variable in the caddytls package and let the httpserver package set it as well as its own HTTPPort variable.) The port numbers required by the ACME challenges HTTP-01 and TLS-SNI-01 are hard-coded into the spec as ports 80 and 443 for good reasons, but the big question is whether they necessarily need to be the HTTP and HTTPS ports. Although the answer is probably no, they chose those ports for convenience and widest compatibility/deployability. So this commit also assumes that the "HTTP port" is necessarily the same port on which to serve the HTTP-01 challenge, and the "HTTPS port" is necessarily the same one on which to serve the TLS-SNI-01 challenge. In other words, changing the HTTP and HTTPS ports also changes the ports the challenges will be served on. If you change the HTTP and HTTPS ports, you are responsible for configuring your system to forward ports 80 and 443 properly. Closes #918 and closes #1293. Also related: #468.
503 lines
15 KiB
Go
503 lines
15 KiB
Go
package caddytls
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io/ioutil"
|
|
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/mholt/caddy"
|
|
"github.com/xenolf/lego/acme"
|
|
)
|
|
|
|
// Config describes how TLS should be configured and used.
|
|
type Config struct {
|
|
// The hostname or class of hostnames this config is
|
|
// designated for; can contain wildcard characters
|
|
// according to RFC 6125 §6.4.3 - this field MUST
|
|
// be set in order for things to work as expected
|
|
Hostname string
|
|
|
|
// Whether TLS is enabled
|
|
Enabled bool
|
|
|
|
// Minimum and maximum protocol versions to allow
|
|
ProtocolMinVersion uint16
|
|
ProtocolMaxVersion uint16
|
|
|
|
// The list of cipher suites; first should be
|
|
// TLS_FALLBACK_SCSV to prevent degrade attacks
|
|
Ciphers []uint16
|
|
|
|
// Whether to prefer server cipher suites
|
|
PreferServerCipherSuites bool
|
|
|
|
// The list of preferred curves
|
|
CurvePreferences []tls.CurveID
|
|
|
|
// Client authentication policy
|
|
ClientAuth tls.ClientAuthType
|
|
|
|
// List of client CA certificates to allow, if
|
|
// client authentication is enabled
|
|
ClientCerts []string
|
|
|
|
// Manual means user provides own certs and keys
|
|
Manual bool
|
|
|
|
// Managed means config qualifies for implicit,
|
|
// automatic, managed TLS; as opposed to the user
|
|
// providing and managing the certificate manually
|
|
Managed bool
|
|
|
|
// OnDemand means the class of hostnames this
|
|
// config applies to may obtain and manage
|
|
// certificates at handshake-time (as opposed
|
|
// to pre-loaded at startup); OnDemand certs
|
|
// will be managed the same way as preloaded
|
|
// ones, however, if an OnDemand cert fails to
|
|
// renew, it is removed from the in-memory
|
|
// cache; if this is true, Managed must
|
|
// necessarily be true
|
|
OnDemand bool
|
|
|
|
// SelfSigned means that this hostname is
|
|
// served with a self-signed certificate
|
|
// that we generated in memory for convenience
|
|
SelfSigned bool
|
|
|
|
// The endpoint of the directory for the ACME
|
|
// CA we are to use
|
|
CAUrl string
|
|
|
|
// The host (ONLY the host, not port) to listen
|
|
// on if necessary to start a listener to solve
|
|
// an ACME challenge
|
|
ListenHost string
|
|
|
|
// The alternate port (ONLY port, not host)
|
|
// to use for the ACME HTTP challenge; this
|
|
// port will be used if we proxy challenges
|
|
// coming in on port 80 to this alternate port
|
|
AltHTTPPort string
|
|
|
|
// The alternate port (ONLY port, not host)
|
|
// to use for the ACME TLS-SNI challenge.
|
|
// The system must forward the standard port
|
|
// for the TLS-SNI challenge to this port.
|
|
AltTLSSNIPort string
|
|
|
|
// The string identifier of the DNS provider
|
|
// to use when solving the ACME DNS challenge
|
|
DNSProvider string
|
|
|
|
// The email address to use when creating or
|
|
// using an ACME account (fun fact: if this
|
|
// is set to "off" then this config will not
|
|
// qualify for managed TLS)
|
|
ACMEEmail string
|
|
|
|
// The type of key to use when generating
|
|
// certificates
|
|
KeyType acme.KeyType
|
|
|
|
// The storage creator; use StorageFor() to get a guaranteed
|
|
// non-nil Storage instance. Note, Caddy may call this frequently
|
|
// so implementors are encouraged to cache any heavy instantiations.
|
|
StorageProvider string
|
|
|
|
// The state needed to operate on-demand TLS
|
|
OnDemandState OnDemandState
|
|
|
|
// Add the must staple TLS extension to the CSR generated by lego/acme
|
|
MustStaple bool
|
|
|
|
// The list of protocols to choose from for Application Layer
|
|
// Protocol Negotiation (ALPN).
|
|
ALPN []string
|
|
|
|
tlsConfig *tls.Config // the final tls.Config created with buildStandardTLSConfig()
|
|
}
|
|
|
|
// OnDemandState contains some state relevant for providing
|
|
// on-demand TLS.
|
|
type OnDemandState struct {
|
|
// The number of certificates that have been issued on-demand
|
|
// by this config. It is only safe to modify this count atomically.
|
|
// If it reaches MaxObtain, on-demand issuances must fail.
|
|
ObtainedCount int32
|
|
|
|
// Set from max_certs in tls config, it specifies the
|
|
// maximum number of certificates that can be issued.
|
|
MaxObtain int32
|
|
}
|
|
|
|
// ObtainCert obtains a certificate for name using c, as long
|
|
// as a certificate does not already exist in storage for that
|
|
// name. The name must qualify and c must be flagged as Managed.
|
|
// This function is a no-op if storage already has a certificate
|
|
// for name.
|
|
//
|
|
// It only obtains and stores certificates (and their keys),
|
|
// it does not load them into memory. If allowPrompts is true,
|
|
// the user may be shown a prompt.
|
|
func (c *Config) ObtainCert(name string, allowPrompts bool) error {
|
|
if !c.Managed || !HostQualifies(name) {
|
|
return nil
|
|
}
|
|
|
|
storage, err := c.StorageFor(c.CAUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
siteExists, err := storage.SiteExists(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if siteExists {
|
|
return nil
|
|
}
|
|
if c.ACMEEmail == "" {
|
|
c.ACMEEmail = getEmail(storage, allowPrompts)
|
|
}
|
|
|
|
client, err := newACMEClient(c, allowPrompts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.Obtain(name)
|
|
}
|
|
|
|
// RenewCert renews the certificate for name using c. It stows the
|
|
// renewed certificate and its assets in storage if successful.
|
|
func (c *Config) RenewCert(name string, allowPrompts bool) error {
|
|
client, err := newACMEClient(c, allowPrompts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.Renew(name)
|
|
}
|
|
|
|
// StorageFor obtains a TLS Storage instance for the given CA URL which should
|
|
// be unique for every different ACME CA. If a StorageCreator is set on this
|
|
// Config, it will be used. Otherwise the default file storage implementation
|
|
// is used. When the error is nil, this is guaranteed to return a non-nil
|
|
// Storage instance.
|
|
func (c *Config) StorageFor(caURL string) (Storage, error) {
|
|
// Validate CA URL
|
|
if caURL == "" {
|
|
caURL = DefaultCAUrl
|
|
}
|
|
if caURL == "" {
|
|
return nil, fmt.Errorf("cannot create storage without CA URL")
|
|
}
|
|
caURL = strings.ToLower(caURL)
|
|
|
|
// scheme required or host will be parsed as path (as of Go 1.6)
|
|
if !strings.Contains(caURL, "://") {
|
|
caURL = "https://" + caURL
|
|
}
|
|
|
|
u, err := url.Parse(caURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: unable to parse CA URL: %v", caURL, err)
|
|
}
|
|
|
|
if u.Host == "" {
|
|
return nil, fmt.Errorf("%s: no host in CA URL", caURL)
|
|
}
|
|
|
|
// Create the storage based on the URL
|
|
var s Storage
|
|
if c.StorageProvider == "" {
|
|
c.StorageProvider = "file"
|
|
}
|
|
|
|
creator, ok := storageProviders[c.StorageProvider]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: Unknown storage: %v", caURL, c.StorageProvider)
|
|
}
|
|
|
|
s, err = creator(u)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: unable to create custom storage '%v': %v", caURL, c.StorageProvider, err)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// buildStandardTLSConfig converts cfg (*caddytls.Config) to a *tls.Config
|
|
// and stores it in cfg so it can be used in servers. If TLS is disabled,
|
|
// no tls.Config is created.
|
|
func (cfg *Config) buildStandardTLSConfig() error {
|
|
if !cfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
config := new(tls.Config)
|
|
|
|
ciphersAdded := make(map[uint16]struct{})
|
|
curvesAdded := make(map[tls.CurveID]struct{})
|
|
|
|
// add cipher suites
|
|
for _, ciph := range cfg.Ciphers {
|
|
if _, ok := ciphersAdded[ciph]; !ok {
|
|
ciphersAdded[ciph] = struct{}{}
|
|
config.CipherSuites = append(config.CipherSuites, ciph)
|
|
}
|
|
}
|
|
|
|
config.PreferServerCipherSuites = cfg.PreferServerCipherSuites
|
|
|
|
// add curve preferences
|
|
for _, curv := range cfg.CurvePreferences {
|
|
if _, ok := curvesAdded[curv]; !ok {
|
|
curvesAdded[curv] = struct{}{}
|
|
config.CurvePreferences = append(config.CurvePreferences, curv)
|
|
}
|
|
}
|
|
|
|
config.MinVersion = cfg.ProtocolMinVersion
|
|
config.MaxVersion = cfg.ProtocolMaxVersion
|
|
config.ClientAuth = cfg.ClientAuth
|
|
config.NextProtos = cfg.ALPN
|
|
config.GetCertificate = cfg.GetCertificate
|
|
|
|
// set up client authentication if enabled
|
|
if config.ClientAuth != tls.NoClientCert {
|
|
pool := x509.NewCertPool()
|
|
clientCertsAdded := make(map[string]struct{})
|
|
|
|
for _, caFile := range cfg.ClientCerts {
|
|
// don't add cert to pool more than once
|
|
if _, ok := clientCertsAdded[caFile]; ok {
|
|
continue
|
|
}
|
|
clientCertsAdded[caFile] = struct{}{}
|
|
|
|
// Any client with a certificate from this CA will be allowed to connect
|
|
caCrt, err := ioutil.ReadFile(caFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !pool.AppendCertsFromPEM(caCrt) {
|
|
return fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile)
|
|
}
|
|
}
|
|
|
|
config.ClientCAs = pool
|
|
}
|
|
|
|
// default cipher suites
|
|
if len(config.CipherSuites) == 0 {
|
|
config.CipherSuites = defaultCiphers
|
|
}
|
|
|
|
// for security, ensure TLS_FALLBACK_SCSV is always included first
|
|
if len(config.CipherSuites) == 0 || config.CipherSuites[0] != tls.TLS_FALLBACK_SCSV {
|
|
config.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.CipherSuites...)
|
|
}
|
|
|
|
// store the resulting new tls.Config
|
|
cfg.tlsConfig = config
|
|
|
|
return nil
|
|
}
|
|
|
|
// MakeTLSConfig makes a tls.Config from configs. The returned
|
|
// tls.Config is programmed to load the matching caddytls.Config
|
|
// based on the hostname in SNI, but that's all.
|
|
func MakeTLSConfig(configs []*Config) (*tls.Config, error) {
|
|
if len(configs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
configMap := make(configGroup)
|
|
|
|
for i, cfg := range configs {
|
|
if cfg == nil {
|
|
// avoid nil pointer dereference below this loop
|
|
configs[i] = new(Config)
|
|
continue
|
|
}
|
|
|
|
// can't serve TLS and non-TLS on same port
|
|
if i > 0 && cfg.Enabled != configs[i-1].Enabled {
|
|
thisConfProto, lastConfProto := "not TLS", "not TLS"
|
|
if cfg.Enabled {
|
|
thisConfProto = "TLS"
|
|
}
|
|
if configs[i-1].Enabled {
|
|
lastConfProto = "TLS"
|
|
}
|
|
return nil, fmt.Errorf("cannot multiplex %s (%s) and %s (%s) on same listener",
|
|
configs[i-1].Hostname, lastConfProto, cfg.Hostname, thisConfProto)
|
|
}
|
|
|
|
// convert each caddytls.Config into a tls.Config
|
|
if err := cfg.buildStandardTLSConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Key this config by its hostname (overwriting
|
|
// configs with the same hostname pattern); during
|
|
// TLS handshakes, configs are loaded based on
|
|
// the hostname pattern, according to client's SNI.
|
|
configMap[cfg.Hostname] = cfg
|
|
}
|
|
|
|
// Is TLS disabled? By now, we know that all
|
|
// configs agree whether it is or not, so we
|
|
// can just look at the first one. If so,
|
|
// we're done here.
|
|
if len(configs) == 0 || !configs[0].Enabled {
|
|
return nil, nil
|
|
}
|
|
|
|
return &tls.Config{
|
|
GetConfigForClient: configMap.GetConfigForClient,
|
|
}, nil
|
|
}
|
|
|
|
// ConfigGetter gets a Config keyed by key.
|
|
type ConfigGetter func(c *caddy.Controller) *Config
|
|
|
|
var configGetters = make(map[string]ConfigGetter)
|
|
|
|
// RegisterConfigGetter registers fn as the way to get a
|
|
// Config for server type serverType.
|
|
func RegisterConfigGetter(serverType string, fn ConfigGetter) {
|
|
configGetters[serverType] = fn
|
|
}
|
|
|
|
// SetDefaultTLSParams sets the default TLS cipher suites, protocol versions,
|
|
// and server preferences of a server.Config if they were not previously set
|
|
// (it does not overwrite; only fills in missing values).
|
|
func SetDefaultTLSParams(config *Config) {
|
|
// If no ciphers provided, use default list
|
|
if len(config.Ciphers) == 0 {
|
|
config.Ciphers = defaultCiphers
|
|
}
|
|
|
|
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
|
|
// (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
|
|
config.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.Ciphers...)
|
|
|
|
// If no curves provided, use default list
|
|
if len(config.CurvePreferences) == 0 {
|
|
config.CurvePreferences = defaultCurves
|
|
}
|
|
|
|
// Set default protocol min and max versions - must balance compatibility and security
|
|
if config.ProtocolMinVersion == 0 {
|
|
config.ProtocolMinVersion = tls.VersionTLS11
|
|
}
|
|
if config.ProtocolMaxVersion == 0 {
|
|
config.ProtocolMaxVersion = tls.VersionTLS12
|
|
}
|
|
|
|
// Prefer server cipher suites
|
|
config.PreferServerCipherSuites = true
|
|
}
|
|
|
|
// Map of supported key types
|
|
var supportedKeyTypes = map[string]acme.KeyType{
|
|
"P384": acme.EC384,
|
|
"P256": acme.EC256,
|
|
"RSA8192": acme.RSA8192,
|
|
"RSA4096": acme.RSA4096,
|
|
"RSA2048": acme.RSA2048,
|
|
}
|
|
|
|
// Map of supported protocols.
|
|
// HTTP/2 only supports TLS 1.2 and higher.
|
|
var supportedProtocols = map[string]uint16{
|
|
"tls1.0": tls.VersionTLS10,
|
|
"tls1.1": tls.VersionTLS11,
|
|
"tls1.2": tls.VersionTLS12,
|
|
}
|
|
|
|
// Map of supported ciphers, used only for parsing config.
|
|
//
|
|
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
|
|
// including all but four of the suites below (the four GCM suites).
|
|
// See https://http2.github.io/http2-spec/#BadCipherSuites
|
|
//
|
|
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
|
|
// it is always added (even though it is not technically a cipher suite).
|
|
//
|
|
// This map, like any map, is NOT ORDERED. Do not range over this map.
|
|
var supportedCiphersMap = map[string]uint16{
|
|
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
"ECDHE-RSA-WITH-CHACHA20-POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
"ECDHE-ECDSA-WITH-CHACHA20-POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
|
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
|
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
|
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
|
}
|
|
|
|
// List of all the ciphers we want to use by default
|
|
var defaultCiphers = []uint16{
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
|
}
|
|
|
|
// Map of supported curves
|
|
// https://golang.org/pkg/crypto/tls/#CurveID
|
|
var supportedCurvesMap = map[string]tls.CurveID{
|
|
"X25519": tls.X25519,
|
|
"P256": tls.CurveP256,
|
|
"P384": tls.CurveP384,
|
|
"P521": tls.CurveP521,
|
|
}
|
|
|
|
// List of all the curves we want to use by default
|
|
//
|
|
// This list should only include curves which are fast by design (e.g. X25519)
|
|
// and those for which an optimized assembly implementation exists (e.g. P256).
|
|
// The latter ones can be found here: https://github.com/golang/go/tree/master/src/crypto/elliptic
|
|
var defaultCurves = []tls.CurveID{
|
|
tls.X25519,
|
|
tls.CurveP256,
|
|
}
|
|
|
|
const (
|
|
// HTTPChallengePort is the officially designated port for
|
|
// the HTTP challenge according to the ACME spec.
|
|
HTTPChallengePort = "80"
|
|
|
|
// TLSSNIChallengePort is the officially designated port for
|
|
// the TLS-SNI challenge according to the ACME spec.
|
|
TLSSNIChallengePort = "443"
|
|
|
|
// DefaultHTTPAlternatePort is the port on which the ACME
|
|
// client will open a listener and solve the HTTP challenge.
|
|
// If this alternate port is used instead of the default
|
|
// port, then whatever is listening on the default port must
|
|
// be capable of proxying or forwarding the request to this
|
|
// alternate port.
|
|
DefaultHTTPAlternatePort = "5033"
|
|
)
|