Rewrote Caddy from the ground up; initial commit of 0.9 branch

These changes span work from the last ~4 months in an effort to make
Caddy more extensible, reduce the coupling between its components, and
lay a more robust foundation of code going forward into 1.0. A bunch of
new features have been added, too, with even higher future potential.

The most significant design change is an overall inversion of
dependencies. Instead of the caddy package knowing about the server
and the notion of middleware and config, the caddy package exposes an
interface that other components plug into. This does introduce more
indirection when reading the code, but every piece is very modular and
pluggable. Even the HTTP server is pluggable.

The caddy package has been moved to the top level, and main has been
pushed into a subfolder called caddy. The actual logic of the main
file has been pushed even further into caddy/caddymain/run.go so that
custom builds of Caddy can be 'go get'able.

The HTTPS logic was surgically separated into two parts to divide the
TLS-specific code and the HTTPS-specific code. The caddytls package can
now be used by any type of server that needs TLS, not just HTTP. I also
added the ability to customize nearly every aspect of TLS at the site
level rather than all sites sharing the same TLS configuration. Not all
of this flexibility is exposed in the Caddyfile yet, but it may be in
the future. Caddy can also generate self-signed certificates in memory
for the convenience of a developer working on localhost who wants HTTPS.
And Caddy now supports the DNS challenge, assuming at least one DNS
provider is plugged in.

Dozens, if not hundreds, of other minor changes swept through the code
base as I literally started from an empty main function, copying over
functions or files as needed, then adjusting them to fit in the new
design. Most tests have been restored and adapted to the new API,
but more work is needed there.

A lot of what was "impossible" before is now possible, or can be made
possible with minimal disruption of the code. For example, it's fairly
easy to make plugins hook into another part of the code via callbacks.
Plugins can do more than just be directives; we now have plugins that
customize how the Caddyfile is loaded (useful when you need to get your
configuration from a remote store).

Site addresses no longer need be just a host and port. They can have a
path, allowing you to scope a configuration to a specific path. There is
no inheretance, however; each site configuration is distinct.

Thanks to amazing work by Lucas Clemente, this commit adds experimental
QUIC support. Turn it on using the -quic flag; your browser may have
to be configured to enable it.

Almost everything is here, but you will notice that most of the middle-
ware are missing. After those are transferred over, we'll be ready for
beta tests.

I'm very excited to get this out. Thanks for everyone's help and
patience these last few months. I hope you like it!!
This commit is contained in:
Matthew Holt
2016-06-04 17:00:29 -06:00
parent e1a6b60736
commit ac4fa2c3a9
230 changed files with 5796 additions and 17422 deletions

237
caddytls/certificates.go Normal file
View File

@ -0,0 +1,237 @@
package caddytls
import (
"crypto/tls"
"crypto/x509"
"errors"
"io/ioutil"
"log"
"strings"
"sync"
"time"
"github.com/xenolf/lego/acme"
"golang.org/x/crypto/ocsp"
)
// certCache stores certificates in memory,
// keying certificates by name.
var certCache = make(map[string]Certificate)
var certCacheMu sync.RWMutex
// Certificate is a tls.Certificate with associated metadata tacked on.
// Even if the metadata can be obtained by parsing the certificate,
// we can be more efficient by extracting the metadata once so it's
// just there, ready to use.
type Certificate struct {
tls.Certificate
// Names is the list of names this certificate is written for.
// The first is the CommonName (if any), the rest are SAN.
Names []string
// NotAfter is when the certificate expires.
NotAfter time.Time
// OCSP contains the certificate's parsed OCSP response.
OCSP *ocsp.Response
// Config is the configuration with which the certificate was
// loaded or obtained and with which it should be maintained.
Config *Config
}
// getCertificate gets a certificate that matches name (a server name)
// from the in-memory cache. If there is no exact match for name, it
// will be checked against names of the form '*.example.com' (wildcard
// certificates) according to RFC 6125. If a match is found, matched will
// be true. If no matches are found, matched will be false and a default
// certificate will be returned with defaulted set to true. If no default
// certificate is set, defaulted will be set to false.
//
// The logic in this function is adapted from the Go standard library,
// which is by the Go Authors.
//
// This function is safe for concurrent use.
func getCertificate(name string) (cert Certificate, matched, defaulted bool) {
var ok bool
// Not going to trim trailing dots here since RFC 3546 says,
// "The hostname is represented ... without a trailing dot."
// Just normalize to lowercase.
name = strings.ToLower(name)
certCacheMu.RLock()
defer certCacheMu.RUnlock()
// exact match? great, let's use it
if cert, ok = certCache[name]; ok {
matched = true
return
}
// try replacing labels in the name with wildcards until we get a match
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
if cert, ok = certCache[candidate]; ok {
matched = true
return
}
}
// if nothing matches, use the default certificate or bust
cert, defaulted = certCache[""]
return
}
// CacheManagedCertificate loads the certificate for domain into the
// cache, flagging it as Managed and, if onDemand is true, as "OnDemand"
// (meaning that it was obtained or loaded during a TLS handshake).
//
// This function is safe for concurrent use.
func CacheManagedCertificate(domain string, cfg *Config) (Certificate, error) {
storage, err := StorageFor(cfg.CAUrl)
if err != nil {
return Certificate{}, err
}
cert, err := makeCertificateFromDisk(storage.SiteCertFile(domain), storage.SiteKeyFile(domain))
if err != nil {
return cert, err
}
cert.Config = cfg
cacheCertificate(cert)
return cert, nil
}
// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
// and keyFile, which must be in PEM format. It stores the certificate in
// memory. The Managed and OnDemand flags of the certificate will be set to
// false.
//
// This function is safe for concurrent use.
func cacheUnmanagedCertificatePEMFile(certFile, keyFile string) error {
cert, err := makeCertificateFromDisk(certFile, keyFile)
if err != nil {
return err
}
cacheCertificate(cert)
return nil
}
// cacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
// of the certificate and key, then caches it in memory.
//
// This function is safe for concurrent use.
func cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error {
cert, err := makeCertificate(certBytes, keyBytes)
if err != nil {
return err
}
cacheCertificate(cert)
return nil
}
// makeCertificateFromDisk makes a Certificate by loading the
// certificate and key files. It fills out all the fields in
// the certificate except for the Managed and OnDemand flags.
// (It is up to the caller to set those.)
func makeCertificateFromDisk(certFile, keyFile string) (Certificate, error) {
certPEMBlock, err := ioutil.ReadFile(certFile)
if err != nil {
return Certificate{}, err
}
keyPEMBlock, err := ioutil.ReadFile(keyFile)
if err != nil {
return Certificate{}, err
}
return makeCertificate(certPEMBlock, keyPEMBlock)
}
// makeCertificate turns a certificate PEM bundle and a key PEM block into
// a Certificate, with OCSP and other relevant metadata tagged with it,
// except for the OnDemand and Managed flags. It is up to the caller to
// set those properties.
func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
var cert Certificate
// Convert to a tls.Certificate
tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return cert, err
}
if len(tlsCert.Certificate) == 0 {
return cert, errors.New("certificate is empty")
}
// Parse leaf certificate and extract relevant metadata
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return cert, err
}
if leaf.Subject.CommonName != "" {
cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)}
}
for _, name := range leaf.DNSNames {
if name != leaf.Subject.CommonName {
cert.Names = append(cert.Names, strings.ToLower(name))
}
}
cert.NotAfter = leaf.NotAfter
// Staple OCSP
ocspBytes, ocspResp, err := acme.GetOCSPForCert(certPEMBlock)
if err != nil {
// An error here is not a problem because a certificate may simply
// not contain a link to an OCSP server. But we should log it anyway.
log.Printf("[WARNING] No OCSP stapling for %v: %v", cert.Names, err)
} else if ocspResp.Status == ocsp.Good {
tlsCert.OCSPStaple = ocspBytes
cert.OCSP = ocspResp
}
cert.Certificate = tlsCert
return cert, nil
}
// cacheCertificate adds cert to the in-memory cache. If the cache is
// empty, cert will be used as the default certificate. If the cache is
// full, random entries are deleted until there is room to map all the
// names on the certificate.
//
// This certificate will be keyed to the names in cert.Names. Any name
// that is already a key in the cache will be replaced with this cert.
//
// This function is safe for concurrent use.
func cacheCertificate(cert Certificate) {
certCacheMu.Lock()
if _, ok := certCache[""]; !ok {
// use as default - must be *appended* to list, or bad things happen!
cert.Names = append(cert.Names, "")
certCache[""] = cert
}
for len(certCache)+len(cert.Names) > 10000 {
// for simplicity, just remove random elements
for key := range certCache {
if key == "" { // ... but not the default cert
continue
}
delete(certCache, key)
break
}
}
for _, name := range cert.Names {
certCache[name] = cert
}
certCacheMu.Unlock()
}
// uncacheCertificate deletes name's certificate from the
// cache. If name is not a key in the certificate cache,
// this function does nothing.
func uncacheCertificate(name string) {
certCacheMu.Lock()
delete(certCache, name)
certCacheMu.Unlock()
}

View File

@ -0,0 +1,59 @@
package caddytls
import "testing"
func TestUnexportedGetCertificate(t *testing.T) {
defer func() { certCache = make(map[string]Certificate) }()
// When cache is empty
if _, matched, defaulted := getCertificate("example.com"); matched || defaulted {
t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted)
}
// When cache has one certificate in it (also is default)
defaultCert := Certificate{Names: []string{"example.com", ""}}
certCache[""] = defaultCert
certCache["example.com"] = defaultCert
if cert, matched, defaulted := getCertificate("Example.com"); !matched || defaulted || cert.Names[0] != "example.com" {
t.Errorf("Didn't get a cert for 'Example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}
if cert, matched, defaulted := getCertificate(""); !matched || defaulted || cert.Names[0] != "example.com" {
t.Errorf("Didn't get a cert for '' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}
// When retrieving wildcard certificate
certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}}
if cert, matched, defaulted := getCertificate("sub.example.com"); !matched || defaulted || cert.Names[0] != "*.example.com" {
t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}
// When no certificate matches, the default is returned
if cert, matched, defaulted := getCertificate("nomatch"); matched || !defaulted {
t.Errorf("Expected matched=false, defaulted=true; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
} else if cert.Names[0] != "example.com" {
t.Errorf("Expected default cert, got: %v", cert)
}
}
func TestCacheCertificate(t *testing.T) {
defer func() { certCache = make(map[string]Certificate) }()
cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}})
if _, ok := certCache["example.com"]; !ok {
t.Error("Expected first cert to be cached by key 'example.com', but it wasn't")
}
if _, ok := certCache["sub.example.com"]; !ok {
t.Error("Expected first cert to be cached by key 'sub.exmaple.com', but it wasn't")
}
if cert, ok := certCache[""]; !ok || cert.Names[2] != "" {
t.Error("Expected first cert to be cached additionally as the default certificate with empty name added, but it wasn't")
}
cacheCertificate(Certificate{Names: []string{"example2.com"}})
if _, ok := certCache["example2.com"]; !ok {
t.Error("Expected second cert to be cached by key 'exmaple2.com', but it wasn't")
}
if cert, ok := certCache[""]; ok && cert.Names[0] == "example2.com" {
t.Error("Expected second cert to NOT be cached as default, but it was")
}
}

298
caddytls/client.go Normal file
View File

@ -0,0 +1,298 @@
package caddytls
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/mholt/caddy"
"github.com/xenolf/lego/acme"
)
// acmeMu ensures that only one ACME challenge occurs at a time.
var acmeMu sync.Mutex
// ACMEClient is an acme.Client with custom state attached.
type ACMEClient struct {
*acme.Client
AllowPrompts bool
config *Config
}
// newACMEClient creates a new ACMEClient given an email and whether
// prompting the user is allowed. It's a variable so we can mock in tests.
var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) {
storage, err := StorageFor(config.CAUrl)
if err != nil {
return nil, err
}
// Look up or create the LE user account
leUser, err := getUser(storage, config.ACMEEmail)
if err != nil {
return nil, err
}
// ensure key type is set
keyType := DefaultKeyType
if config.KeyType != "" {
keyType = config.KeyType
}
// ensure CA URL (directory endpoint) is set
caURL := DefaultCAUrl
if config.CAUrl != "" {
caURL = config.CAUrl
}
// ensure endpoint is secure (assume HTTPS if scheme is missing)
if !strings.Contains(caURL, "://") {
caURL = "https://" + caURL
}
u, err := url.Parse(caURL)
if u.Scheme != "https" &&
u.Host != "localhost" &&
u.Host != "[::1]" &&
!strings.HasPrefix(u.Host, "127.") &&
!strings.HasPrefix(u.Host, "10.") {
return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL)
}
// The client facilitates our communication with the CA server.
client, err := acme.NewClient(caURL, &leUser, keyType)
if err != nil {
return nil, err
}
// If not registered, the user must register an account with the CA
// and agree to terms
if leUser.Registration == nil {
reg, err := client.Register()
if err != nil {
return nil, errors.New("registration error: " + err.Error())
}
leUser.Registration = reg
if allowPrompts { // can't prompt a user who isn't there
if !Agreed && reg.TosURL == "" {
Agreed = promptUserAgreement(saURL, false) // TODO - latest URL
}
if !Agreed && reg.TosURL == "" {
return nil, errors.New("user must agree to terms")
}
}
err = client.AgreeToTOS()
if err != nil {
saveUser(storage, leUser) // Might as well try, right?
return nil, errors.New("error agreeing to terms: " + err.Error())
}
// save user to the file system
err = saveUser(storage, leUser)
if err != nil {
return nil, errors.New("could not save user: " + err.Error())
}
}
c := &ACMEClient{Client: client, AllowPrompts: allowPrompts, config: config}
if config.DNSProvider == "" {
// Use HTTP and TLS-SNI challenges by default
// See if HTTP challenge needs to be proxied
if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, HTTPChallengePort)) {
altPort := config.AltHTTPPort
if altPort == "" {
altPort = DefaultHTTPAlternatePort
}
c.SetHTTPAddress(net.JoinHostPort(config.ListenHost, altPort))
}
// See if TLS challenge needs to be handled by our own facilities
if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, TLSSNIChallengePort)) {
c.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{})
}
} else {
// Otherwise, DNS challenge it is
// Load provider constructor function
provFn, ok := dnsProviders[config.DNSProvider]
if !ok {
return nil, errors.New("unknown DNS provider by name '" + config.DNSProvider + "'")
}
// we could pass credentials to create the provider, but for now
// we just let the solver package get them from the environment
prov, err := provFn()
if err != nil {
return nil, err
}
// Use the DNS challenge exclusively
c.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
c.SetChallengeProvider(acme.DNS01, prov)
}
return c, nil
}
// Obtain obtains a single certificate for names. It stores the certificate
// on the disk if successful.
func (c *ACMEClient) Obtain(names []string) error {
Attempts:
for attempts := 0; attempts < 2; attempts++ {
acmeMu.Lock()
certificate, failures := c.ObtainCertificate(names, true, nil)
acmeMu.Unlock()
if len(failures) > 0 {
// Error - try to fix it or report it to the user and abort
var errMsg string // we'll combine all the failures into a single error message
var promptedForAgreement bool // only prompt user for agreement at most once
for errDomain, obtainErr := range failures {
if obtainErr == nil {
continue
}
if tosErr, ok := obtainErr.(acme.TOSError); ok {
// Terms of Service agreement error; we can probably deal with this
if !Agreed && !promptedForAgreement && c.AllowPrompts {
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
promptedForAgreement = true
}
if Agreed || !c.AllowPrompts {
err := c.AgreeToTOS()
if err != nil {
return errors.New("error agreeing to updated terms: " + err.Error())
}
continue Attempts
}
}
// If user did not agree or it was any other kind of error, just append to the list of errors
errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
}
return errors.New(errMsg)
}
// Success - immediately save the certificate resource
storage, err := StorageFor(c.config.CAUrl)
if err != nil {
return err
}
err = saveCertResource(storage, certificate)
if err != nil {
return fmt.Errorf("error saving assets for %v: %v", names, err)
}
break
}
return nil
}
// Renew renews the managed certificate for name. Right now our storage
// mechanism only supports one name per certificate, so this function only
// accepts one domain as input. It can be easily modified to support SAN
// certificates if, one day, they become desperately needed enough that our
// storage mechanism is upgraded to be more complex to support SAN certs.
//
// Anyway, this function is safe for concurrent use.
func (c *ACMEClient) Renew(name string) error {
// Get access to ACME storage
storage, err := StorageFor(c.config.CAUrl)
if err != nil {
return err
}
// Prepare for renewal (load PEM cert, key, and meta)
certBytes, err := ioutil.ReadFile(storage.SiteCertFile(name))
if err != nil {
return err
}
keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(name))
if err != nil {
return err
}
metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(name))
if err != nil {
return err
}
var certMeta acme.CertificateResource
err = json.Unmarshal(metaBytes, &certMeta)
certMeta.Certificate = certBytes
certMeta.PrivateKey = keyBytes
// Perform renewal and retry if necessary, but not too many times.
var newCertMeta acme.CertificateResource
var success bool
for attempts := 0; attempts < 2; attempts++ {
acmeMu.Lock()
newCertMeta, err = c.RenewCertificate(certMeta, true)
acmeMu.Unlock()
if err == nil {
success = true
break
}
// If the legal terms changed and need to be agreed to again,
// we can handle that.
if _, ok := err.(acme.TOSError); ok {
err := c.AgreeToTOS()
if err != nil {
return err
}
continue
}
// For any other kind of error, wait 10s and try again.
wait := 10 * time.Second
log.Printf("[ERROR] Renewing: %v; trying again in %s", err, wait)
time.Sleep(wait)
}
if !success {
return errors.New("too many renewal attempts; last error: " + err.Error())
}
return saveCertResource(storage, newCertMeta)
}
// Revoke revokes the certificate for name and deltes
// it from storage.
func (c *ACMEClient) Revoke(name string) error {
storage, err := StorageFor(c.config.CAUrl)
if err != nil {
return err
}
if !existingCertAndKey(storage, name) {
return errors.New("no certificate and key for " + name)
}
certFile := storage.SiteCertFile(name)
certBytes, err := ioutil.ReadFile(certFile)
if err != nil {
return err
}
err = c.Client.RevokeCertificate(certBytes)
if err != nil {
return err
}
err = os.Remove(certFile)
if err != nil {
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
}
return nil
}

3
caddytls/client_test.go Normal file
View File

@ -0,0 +1,3 @@
package caddytls
// TODO

437
caddytls/config.go Normal file
View File

@ -0,0 +1,437 @@
package caddytls
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"time"
"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
// NOT be empty in order for things to work smoothly
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
// 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 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 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
}
// ObtainCert obtains a certificate for c.Hostname, as long as a certificate
// does not already exist in storage on disk. It only obtains and stores
// certificates (and their keys) to disk, it does not load them into memory.
// If allowPrompts is true, the user may be shown a prompt. If proxyACME is
// true, the relevant ACME challenges will be proxied to the alternate port.
func (c *Config) ObtainCert(allowPrompts bool) error {
return c.obtainCertName(c.Hostname, allowPrompts)
}
func (c *Config) obtainCertName(name string, allowPrompts bool) error {
storage, err := StorageFor(c.CAUrl)
if err != nil {
return err
}
if !c.Managed || !HostQualifies(name) || existingCertAndKey(storage, name) {
return nil
}
if c.ACMEEmail == "" {
c.ACMEEmail = getEmail(storage, allowPrompts)
}
client, err := newACMEClient(c, allowPrompts)
if err != nil {
return err
}
return client.Obtain([]string{name})
}
// RenewCert renews the certificate for c.Hostname.
func (c *Config) RenewCert(allowPrompts bool) error {
return c.renewCertName(c.Hostname, allowPrompts)
}
func (c *Config) renewCertName(name string, allowPrompts bool) error {
storage, err := StorageFor(c.CAUrl)
if err != nil {
return err
}
// Prepare for renewal (load PEM cert, key, and meta)
certBytes, err := ioutil.ReadFile(storage.SiteCertFile(c.Hostname))
if err != nil {
return err
}
keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(c.Hostname))
if err != nil {
return err
}
metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(c.Hostname))
if err != nil {
return err
}
var certMeta acme.CertificateResource
err = json.Unmarshal(metaBytes, &certMeta)
certMeta.Certificate = certBytes
certMeta.PrivateKey = keyBytes
client, err := newACMEClient(c, allowPrompts)
if err != nil {
return err
}
// Perform renewal and retry if necessary, but not too many times.
var newCertMeta acme.CertificateResource
var success bool
for attempts := 0; attempts < 2; attempts++ {
acmeMu.Lock()
newCertMeta, err = client.RenewCertificate(certMeta, true)
acmeMu.Unlock()
if err == nil {
success = true
break
}
// If the legal terms were updated and need to be
// agreed to again, we can handle that.
if _, ok := err.(acme.TOSError); ok {
err := client.AgreeToTOS()
if err != nil {
return err
}
continue
}
// For any other kind of error, wait 10s and try again.
time.Sleep(10 * time.Second)
}
if !success {
return errors.New("too many renewal attempts; last error: " + err.Error())
}
return saveCertResource(storage, newCertMeta)
}
// MakeTLSConfig reduces configs into a single tls.Config.
// If TLS is to be disabled, a nil tls.Config will be returned.
func MakeTLSConfig(configs []*Config) (*tls.Config, error) {
if configs == nil || len(configs) == 0 {
return nil, nil
}
config := new(tls.Config)
ciphersAdded := make(map[uint16]struct{})
configMap := make(configGroup)
for i, cfg := range configs {
if cfg == nil {
// avoid nil pointer dereference below
configs[i] = new(Config)
continue
}
// Key this config by its hostname; this
// overwrites configs with the same hostname
configMap[cfg.Hostname] = cfg
// Can't serve TLS and not-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)
}
// Union cipher suites
for _, ciph := range cfg.Ciphers {
if _, ok := ciphersAdded[ciph]; !ok {
ciphersAdded[ciph] = struct{}{}
config.CipherSuites = append(config.CipherSuites, ciph)
}
}
// Can't resolve conflicting PreferServerCipherSuites settings
if i > 0 && cfg.PreferServerCipherSuites != configs[i-1].PreferServerCipherSuites {
return nil, fmt.Errorf("cannot both use PreferServerCipherSuites and not use it")
}
// Go with the widest range of protocol versions
if cfg.ProtocolMinVersion < config.MinVersion {
config.MinVersion = cfg.ProtocolMinVersion
}
if cfg.ProtocolMaxVersion < config.MaxVersion {
config.MaxVersion = cfg.ProtocolMaxVersion
}
// Go with the strictest ClientAuth type
if cfg.ClientAuth > config.ClientAuth {
config.ClientAuth = cfg.ClientAuth
}
}
// Is TLS disabled? If so, we're done here.
// By now, we know that all configs agree
// whether it is or not, so we can just look
// at the first one.
if len(configs) == 0 || !configs[0].Enabled {
return nil, nil
}
// Default cipher suites
if len(config.CipherSuites) == 0 {
config.CipherSuites = defaultCiphers
}
// For security, ensure TLS_FALLBACK_SCSV is always included
if config.CipherSuites[0] != tls.TLS_FALLBACK_SCSV {
config.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.CipherSuites...)
}
// Set up client authentication if enabled
if config.ClientAuth != tls.NoClientCert {
pool := x509.NewCertPool()
clientCertsAdded := make(map[string]struct{})
for _, cfg := range configs {
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 nil, err
}
if !pool.AppendCertsFromPEM(caCrt) {
return nil, fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile)
}
}
}
config.ClientCAs = pool
}
// Associate the GetCertificate callback, or almost nothing we just did will work
config.GetCertificate = configMap.GetCertificate
return config, nil
}
// ConfigGetter gets a Config keyed by key.
type ConfigGetter func(key string) *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...)
// 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-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 supported cipher suites in descending order of preference.
// Ordering is very important! Getting the wrong order will break
// mainstream clients, especially with HTTP/2.
//
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
// added manually.
var supportedCiphers = []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_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,
tls.TLS_ECDHE_RSA_WITH_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_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,
}
const (
// HTTPChallengePort is the officially designated port for
// the HTTP challenge.
HTTPChallengePort = "80"
// TLSSNIChallengePort is the officially designated port for
// the TLS-SNI challenge.
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"
)

258
caddytls/crypto.go Normal file
View File

@ -0,0 +1,258 @@
package caddytls
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net"
"os"
"time"
"github.com/xenolf/lego/acme"
)
// loadPrivateKey loads a PEM-encoded ECC/RSA private key from file.
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
switch keyBlock.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(keyBlock.Bytes)
}
return nil, errors.New("unknown private key type")
}
// savePrivateKey saves a PEM-encoded ECC/RSA private key to file.
func savePrivateKey(key crypto.PrivateKey, file string) error {
var pemType string
var keyBytes []byte
switch key := key.(type) {
case *ecdsa.PrivateKey:
var err error
pemType = "EC"
keyBytes, err = x509.MarshalECPrivateKey(key)
if err != nil {
return err
}
case *rsa.PrivateKey:
pemType = "RSA"
keyBytes = x509.MarshalPKCS1PrivateKey(key)
}
pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
keyOut, err := os.Create(file)
if err != nil {
return err
}
keyOut.Chmod(0600)
defer keyOut.Close()
return pem.Encode(keyOut, &pemKey)
}
// stapleOCSP staples OCSP information to cert for hostname name.
// If you have it handy, you should pass in the PEM-encoded certificate
// bundle; otherwise the DER-encoded cert will have to be PEM-encoded.
// If you don't have the PEM blocks handy, just pass in nil.
//
// Errors here are not necessarily fatal, it could just be that the
// certificate doesn't have an issuer URL.
func stapleOCSP(cert *Certificate, pemBundle []byte) error {
if pemBundle == nil {
// The function in the acme package that gets OCSP requires a PEM-encoded cert
bundle := new(bytes.Buffer)
for _, derBytes := range cert.Certificate.Certificate {
pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
}
pemBundle = bundle.Bytes()
}
ocspBytes, ocspResp, err := acme.GetOCSPForCert(pemBundle)
if err != nil {
return err
}
cert.Certificate.OCSPStaple = ocspBytes
cert.OCSP = ocspResp
return nil
}
// makeSelfSignedCert makes a self-signed certificate according
// to the parameters in config. It then caches the certificate
// in our cache.
func makeSelfSignedCert(config *Config) error {
// start by generating private key
var privKey interface{}
var err error
switch config.KeyType {
case "", acme.EC256:
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case acme.EC384:
privKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case acme.RSA2048:
privKey, err = rsa.GenerateKey(rand.Reader, 2048)
case acme.RSA4096:
privKey, err = rsa.GenerateKey(rand.Reader, 4096)
case acme.RSA8192:
privKey, err = rsa.GenerateKey(rand.Reader, 8192)
default:
return fmt.Errorf("cannot generate private key; unknown key type %v", config.KeyType)
}
if err != nil {
return fmt.Errorf("failed to generate private key: %v", err)
}
// create certificate structure with proper values
notBefore := time.Now()
notAfter := notBefore.Add(24 * time.Hour * 7)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %v", err)
}
cert := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{Organization: []string{"Caddy Self-Signed"}},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
if ip := net.ParseIP(config.Hostname); ip != nil {
cert.IPAddresses = append(cert.IPAddresses, ip)
} else {
cert.DNSNames = append(cert.DNSNames, config.Hostname)
}
publicKey := func(privKey interface{}) interface{} {
switch k := privKey.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
default:
return errors.New("unknown key type")
}
}
derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey(privKey), privKey)
if err != nil {
return fmt.Errorf("could not create certificate: %v", err)
}
cacheCertificate(Certificate{
Certificate: tls.Certificate{
Certificate: [][]byte{derBytes},
PrivateKey: privKey,
Leaf: cert,
},
Names: cert.DNSNames,
NotAfter: cert.NotAfter,
Config: config,
})
return nil
}
// RotateSessionTicketKeys rotates the TLS session ticket keys
// on cfg every TicketRotateInterval. It spawns a new goroutine so
// this function does NOT block. It returns a channel you should
// close when you are ready to stop the key rotation, like when the
// server using cfg is no longer running.
func RotateSessionTicketKeys(cfg *tls.Config) chan struct{} {
ch := make(chan struct{})
ticker := time.NewTicker(TicketRotateInterval)
go runTLSTicketKeyRotation(cfg, ticker, ch)
return ch
}
// Functions that may be swapped out for testing
var (
runTLSTicketKeyRotation = standaloneTLSTicketKeyRotation
setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte { return keys }
)
// standaloneTLSTicketKeyRotation governs over the array of TLS ticket keys used to de/crypt TLS tickets.
// It periodically sets a new ticket key as the first one, used to encrypt (and decrypt),
// pushing any old ticket keys to the back, where they are considered for decryption only.
//
// Lack of entropy for the very first ticket key results in the feature being disabled (as does Go),
// later lack of entropy temporarily disables ticket key rotation.
// Old ticket keys are still phased out, though.
//
// Stops the ticker when returning.
func standaloneTLSTicketKeyRotation(c *tls.Config, ticker *time.Ticker, exitChan chan struct{}) {
defer ticker.Stop()
// The entire page should be marked as sticky, but Go cannot do that
// without resorting to syscall#Mlock. And, we don't have madvise (for NODUMP), too. ☹
keys := make([][32]byte, 1, NumTickets)
rng := c.Rand
if rng == nil {
rng = rand.Reader
}
if _, err := io.ReadFull(rng, keys[0][:]); err != nil {
c.SessionTicketsDisabled = true // bail if we don't have the entropy for the first one
return
}
c.SessionTicketKey = keys[0] // SetSessionTicketKeys doesn't set a 'tls.keysAlreadySet'
c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys))
for {
select {
case _, isOpen := <-exitChan:
if !isOpen {
return
}
case <-ticker.C:
rng = c.Rand // could've changed since the start
if rng == nil {
rng = rand.Reader
}
var newTicketKey [32]byte
_, err := io.ReadFull(rng, newTicketKey[:])
if len(keys) < NumTickets {
keys = append(keys, keys[0]) // manipulates the internal length
}
for idx := len(keys) - 1; idx >= 1; idx-- {
keys[idx] = keys[idx-1] // yes, this makes copies
}
if err == nil {
keys[0] = newTicketKey
}
// pushes the last key out, doesn't matter that we don't have a new one
c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys))
}
}
}
const (
// NumTickets is how many tickets to hold and consider
// to decrypt TLS sessions.
NumTickets = 4
// TicketRotateInterval is how often to generate
// new ticket for TLS PFS encryption
TicketRotateInterval = 10 * time.Hour
)

166
caddytls/crypto_test.go Normal file
View File

@ -0,0 +1,166 @@
package caddytls
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"os"
"runtime"
"testing"
"time"
)
func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
keyFile := "test.key"
defer os.Remove(keyFile)
privateKey, err := rsa.GenerateKey(rand.Reader, 128) // make tests faster; small key size OK for testing
if err != nil {
t.Fatal(err)
}
// test save
err = savePrivateKey(privateKey, keyFile)
if err != nil {
t.Fatal("error saving private key:", err)
}
// it doesn't make sense to test file permission on windows
if runtime.GOOS != "windows" {
// get info of the key file
info, err := os.Stat(keyFile)
if err != nil {
t.Fatal("error stating private key:", err)
}
// verify permission of key file is correct
if info.Mode().Perm() != 0600 {
t.Error("Expected key file to have permission 0600, but it wasn't")
}
}
// test load
loadedKey, err := loadPrivateKey(keyFile)
if err != nil {
t.Error("error loading private key:", err)
}
// verify loaded key is correct
if !PrivateKeysSame(privateKey, loadedKey) {
t.Error("Expected key bytes to be the same, but they weren't")
}
}
func TestSaveAndLoadECCPrivateKey(t *testing.T) {
keyFile := "test.key"
defer os.Remove(keyFile)
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
t.Fatal(err)
}
// test save
err = savePrivateKey(privateKey, keyFile)
if err != nil {
t.Fatal("error saving private key:", err)
}
// it doesn't make sense to test file permission on windows
if runtime.GOOS != "windows" {
// get info of the key file
info, err := os.Stat(keyFile)
if err != nil {
t.Fatal("error stating private key:", err)
}
// verify permission of key file is correct
if info.Mode().Perm() != 0600 {
t.Error("Expected key file to have permission 0600, but it wasn't")
}
}
// test load
loadedKey, err := loadPrivateKey(keyFile)
if err != nil {
t.Error("error loading private key:", err)
}
// verify loaded key is correct
if !PrivateKeysSame(privateKey, loadedKey) {
t.Error("Expected key bytes to be the same, but they weren't")
}
}
// PrivateKeysSame compares the bytes of a and b and returns true if they are the same.
func PrivateKeysSame(a, b crypto.PrivateKey) bool {
return bytes.Equal(PrivateKeyBytes(a), PrivateKeyBytes(b))
}
// PrivateKeyBytes returns the bytes of DER-encoded key.
func PrivateKeyBytes(key crypto.PrivateKey) []byte {
var keyBytes []byte
switch key := key.(type) {
case *rsa.PrivateKey:
keyBytes = x509.MarshalPKCS1PrivateKey(key)
case *ecdsa.PrivateKey:
keyBytes, _ = x509.MarshalECPrivateKey(key)
}
return keyBytes
}
func TestStandaloneTLSTicketKeyRotation(t *testing.T) {
tlsGovChan := make(chan struct{})
defer close(tlsGovChan)
callSync := make(chan bool, 1)
defer close(callSync)
oldHook := setSessionTicketKeysTestHook
defer func() {
setSessionTicketKeysTestHook = oldHook
}()
var keysInUse [][32]byte
setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte {
keysInUse = keys
callSync <- true
return keys
}
c := new(tls.Config)
timer := time.NewTicker(time.Millisecond * 1)
go standaloneTLSTicketKeyRotation(c, timer, tlsGovChan)
rounds := 0
var lastTicketKey [32]byte
for {
select {
case <-callSync:
if lastTicketKey == keysInUse[0] {
close(tlsGovChan)
t.Errorf("The same TLS ticket key has been used again (not rotated): %x.", lastTicketKey)
return
}
lastTicketKey = keysInUse[0]
rounds++
if rounds <= NumTickets && len(keysInUse) != rounds {
close(tlsGovChan)
t.Errorf("Expected TLS ticket keys in use: %d; Got instead: %d.", rounds, len(keysInUse))
return
}
if c.SessionTicketsDisabled == true {
t.Error("Session tickets have been disabled unexpectedly.")
return
}
if rounds >= NumTickets+1 {
return
}
case <-time.After(time.Second * 1):
t.Errorf("Timeout after %d rounds.", rounds)
return
}
}
}

312
caddytls/handshake.go Normal file
View File

@ -0,0 +1,312 @@
package caddytls
import (
"crypto/tls"
"errors"
"fmt"
"log"
"strings"
"sync"
"sync/atomic"
"time"
)
// configGroup is a type that keys configs by their hostname
// (hostnames can have wildcard characters; use the getConfig
// method to get a config by matching its hostname). Its
// GetCertificate function can be used with tls.Config.
type configGroup map[string]*Config
// getConfig gets the config by the first key match for name.
// In other words, "sub.foo.bar" will get the config for "*.foo.bar"
// if that is the closest match. This function MAY return nil
// if no match is found.
//
// This function follows nearly the same logic to lookup
// a hostname as the getCertificate function uses.
func (cg configGroup) getConfig(name string) *Config {
name = strings.ToLower(name)
// exact match? great, let's use it
if config, ok := cg[name]; ok {
return config
}
// try replacing labels in the name with wildcards until we get a match
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
if config, ok := cg[candidate]; ok {
return config
}
}
// as last resort, try a config that serves all names
if config, ok := cg[""]; ok {
return config
}
return nil
}
// GetCertificate gets a certificate to satisfy clientHello. In getting
// the certificate, it abides the rules and settings defined in the
// Config that matches clientHello.ServerName. It first checks the in-
// memory cache, then, if the config enables "OnDemand", it accessses
// disk, then accesses the network if it must obtain a new certificate
// via ACME.
//
// This method is safe for use as a tls.Config.GetCertificate callback.
func (cg configGroup) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := cg.getCertDuringHandshake(clientHello.ServerName, true, true)
return &cert.Certificate, err
}
// getCertDuringHandshake will get a certificate for name. It first tries
// the in-memory cache. If no certificate for name is in the cache, the
// config most closely corresponding to name will be loaded. If that config
// allows it (OnDemand==true) and if loadIfNecessary == true, it goes to disk
// to load it into the cache and serve it. If it's not on disk and if
// obtainIfNecessary == true, the certificate will be obtained from the CA,
// cached, and served. If obtainIfNecessary is true, then loadIfNecessary
// must also be set to true. An error will be returned if and only if no
// certificate is available.
//
// This function is safe for concurrent use.
func (cg configGroup) getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
// First check our in-memory cache to see if we've already loaded it
cert, matched, defaulted := getCertificate(name)
if matched {
return cert, nil
}
// Get the relevant TLS config for this name. If OnDemand is enabled,
// then we might be able to load or obtain a needed certificate.
cfg := cg.getConfig(name)
if cfg != nil && cfg.OnDemand && loadIfNecessary {
// Then check to see if we have one on disk
loadedCert, err := CacheManagedCertificate(name, cfg)
if err == nil {
loadedCert, err = cg.handshakeMaintenance(name, loadedCert)
if err != nil {
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
}
return loadedCert, nil
}
if obtainIfNecessary {
// By this point, we need to ask the CA for a certificate
name = strings.ToLower(name)
// Make sure aren't over any applicable limits
err := cg.checkLimitsForObtainingNewCerts(name)
if err != nil {
return Certificate{}, err
}
// Name has to qualify for a certificate
if !HostQualifies(name) {
return cert, errors.New("hostname '" + name + "' does not qualify for certificate")
}
// Obtain certificate from the CA
return cg.obtainOnDemandCertificate(name, cfg)
}
}
// Fall back to the default certificate if there is one
if defaulted {
return cert, nil
}
return Certificate{}, fmt.Errorf("no certificate available for %s", name)
}
// checkLimitsForObtainingNewCerts checks to see if name can be issued right
// now according to mitigating factors we keep track of and preferences the
// user has set. If a non-nil error is returned, do not issue a new certificate
// for name.
func (cg configGroup) checkLimitsForObtainingNewCerts(name string) error {
// User can set hard limit for number of certs for the process to issue
if onDemandMaxIssue > 0 && atomic.LoadInt32(OnDemandIssuedCount) >= onDemandMaxIssue {
return fmt.Errorf("%s: maximum certificates issued (%d)", name, onDemandMaxIssue)
}
// Make sure name hasn't failed a challenge recently
failedIssuanceMu.RLock()
when, ok := failedIssuance[name]
failedIssuanceMu.RUnlock()
if ok {
return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
}
// Make sure, if we've issued a few certificates already, that we haven't
// issued any recently
lastIssueTimeMu.Lock()
since := time.Since(lastIssueTime)
lastIssueTimeMu.Unlock()
if atomic.LoadInt32(OnDemandIssuedCount) >= 10 && since < 10*time.Minute {
return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
}
// 👍Good to go
return nil
}
// obtainOnDemandCertificate obtains a certificate for name for the given
// name. If another goroutine has already started obtaining a cert for
// name, it will wait and use what the other goroutine obtained.
//
// This function is safe for use by multiple concurrent goroutines.
func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certificate, error) {
// We must protect this process from happening concurrently, so synchronize.
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already obtaining the certificate.
// wait for it to finish obtaining the cert and then we'll use it.
obtainCertWaitChansMu.Unlock()
<-wait
return cg.getCertDuringHandshake(name, true, false)
}
// looks like it's up to us to do all the work and obtain the cert
wait = make(chan struct{})
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
// Unblock waiters and delete waitgroup when we return
defer func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}()
log.Printf("[INFO] Obtaining new certificate for %s", name)
if err := cfg.obtainCertName(name, false); err != nil {
// Failed to solve challenge, so don't allow another on-demand
// issue for this name to be attempted for a little while.
failedIssuanceMu.Lock()
failedIssuance[name] = time.Now()
go func(name string) {
time.Sleep(5 * time.Minute)
failedIssuanceMu.Lock()
delete(failedIssuance, name)
failedIssuanceMu.Unlock()
}(name)
failedIssuanceMu.Unlock()
return Certificate{}, err
}
// Success - update counters and stuff
atomic.AddInt32(OnDemandIssuedCount, 1)
lastIssueTimeMu.Lock()
lastIssueTime = time.Now()
lastIssueTimeMu.Unlock()
// The certificate is already on disk; now just start over to load it and serve it
return cg.getCertDuringHandshake(name, true, false)
}
// handshakeMaintenance performs a check on cert for expiration and OCSP
// validity.
//
// This function is safe for use by multiple concurrent goroutines.
func (cg configGroup) handshakeMaintenance(name string, cert Certificate) (Certificate, error) {
// Check cert expiration
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < RenewDurationBefore {
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
return cg.renewDynamicCertificate(name, cert.Config)
}
// Check OCSP staple validity
if cert.OCSP != nil {
refreshTime := cert.OCSP.ThisUpdate.Add(cert.OCSP.NextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
if time.Now().After(refreshTime) {
err := stapleOCSP(&cert, nil)
if err != nil {
// An error with OCSP stapling is not the end of the world, and in fact, is
// quite common considering not all certs have issuer URLs that support it.
log.Printf("[ERROR] Getting OCSP for %s: %v", name, err)
}
certCacheMu.Lock()
certCache[name] = cert
certCacheMu.Unlock()
}
}
return cert, nil
}
// renewDynamicCertificate renews the certificate for name using cfg. It returns the
// certificate to use and an error, if any. currentCert may be returned even if an
// error occurs, since we perform renewals before they expire and it may still be
// usable. name should already be lower-cased before calling this function.
//
// This function is safe for use by multiple concurrent goroutines.
func (cg configGroup) renewDynamicCertificate(name string, cfg *Config) (Certificate, error) {
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already renewing the certificate.
// wait for it to finish, then we'll use the new one.
obtainCertWaitChansMu.Unlock()
<-wait
return cg.getCertDuringHandshake(name, true, false)
}
// looks like it's up to us to do all the work and renew the cert
wait = make(chan struct{})
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
// unblock waiters and delete waitgroup when we return
defer func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}()
log.Printf("[INFO] Renewing certificate for %s", name)
err := cfg.renewCertName(name, false)
if err != nil {
return Certificate{}, err
}
return cg.getCertDuringHandshake(name, true, false)
}
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
var obtainCertWaitChans = make(map[string]chan struct{})
var obtainCertWaitChansMu sync.Mutex
// OnDemandIssuedCount is the number of certificates that have been issued
// on-demand by this process. It is only safe to modify this count atomically.
// If it reaches onDemandMaxIssue, on-demand issuances will fail.
var OnDemandIssuedCount = new(int32)
// onDemandMaxIssue is set based on max_certs in tls config. It specifies the
// maximum number of certificates that can be issued.
// TODO: This applies globally, but we should probably make a server-specific
// way to keep track of these limits and counts, since it's specified in the
// Caddyfile...
var onDemandMaxIssue int32
// failedIssuance is a set of names that we recently failed to get a
// certificate for from the ACME CA. They are removed after some time.
// When a name is in this map, do not issue a certificate for it on-demand.
var failedIssuance = make(map[string]time.Time)
var failedIssuanceMu sync.RWMutex
// lastIssueTime records when we last obtained a certificate successfully.
// If this value is recent, do not make any on-demand certificate requests.
var lastIssueTime time.Time
var lastIssueTimeMu sync.Mutex
var errNoCert = errors.New("no certificate available")

View File

@ -0,0 +1,56 @@
package caddytls
import (
"crypto/tls"
"crypto/x509"
"testing"
)
func TestGetCertificate(t *testing.T) {
defer func() { certCache = make(map[string]Certificate) }()
cg := make(configGroup)
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"}
helloNoSNI := &tls.ClientHelloInfo{}
helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"}
// When cache is empty
if cert, err := cg.GetCertificate(hello); err == nil {
t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert)
}
if cert, err := cg.GetCertificate(helloNoSNI); err == nil {
t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert)
}
// When cache has one certificate in it (also is default)
defaultCert := Certificate{Names: []string{"example.com", ""}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}}
certCache[""] = defaultCert
certCache["example.com"] = defaultCert
if cert, err := cg.GetCertificate(hello); err != nil {
t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err)
} else if cert.Leaf.DNSNames[0] != "example.com" {
t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert)
}
if cert, err := cg.GetCertificate(helloNoSNI); err != nil {
t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err)
} else if cert.Leaf.DNSNames[0] != "example.com" {
t.Errorf("Got wrong certificate for no SNI; expected 'example.com' as default, got: %v", cert)
}
// When retrieving wildcard certificate
certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}}
if cert, err := cg.GetCertificate(helloSub); err != nil {
t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err)
} else if cert.Leaf.DNSNames[0] != "*.example.com" {
t.Errorf("Got wrong certificate, expected wildcard: %v", cert)
}
// When no certificate matches, the default is returned
if cert, err := cg.GetCertificate(helloNoMatch); err != nil {
t.Errorf("Expected default certificate with no error when no matches, got err: %v", err)
} else if cert.Leaf.DNSNames[0] != "example.com" {
t.Errorf("Expected default cert with no matches, got: %v", cert)
}
}

42
caddytls/httphandler.go Normal file
View File

@ -0,0 +1,42 @@
package caddytls
import (
"crypto/tls"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
const challengeBasePath = "/.well-known/acme-challenge"
// HTTPChallengeHandler proxies challenge requests to ACME client if the
// request path starts with challengeBasePath. It returns true if it
// handled the request and no more needs to be done; it returns false
// if this call was a no-op and the request still needs handling.
func HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, altPort string) bool {
if !strings.HasPrefix(r.URL.Path, challengeBasePath) {
return false
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
upstream, err := url.Parse(scheme + "://localhost:" + altPort)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("[ERROR] ACME proxy handler: %v", err)
return true
}
proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
}
proxy.ServeHTTP(w, r)
return true
}

View File

@ -0,0 +1,63 @@
package caddytls
import (
"net"
"net/http"
"net/http/httptest"
"testing"
)
func TestHTTPChallengeHandlerNoOp(t *testing.T) {
// try base paths that aren't handled by this handler
for _, url := range []string{
"http://localhost/",
"http://localhost/foo.html",
"http://localhost/.git",
"http://localhost/.well-known/",
"http://localhost/.well-known/acme-challenging",
} {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatalf("Could not craft request, got error: %v", err)
}
rw := httptest.NewRecorder()
if HTTPChallengeHandler(rw, req, DefaultHTTPAlternatePort) {
t.Errorf("Got true with this URL, but shouldn't have: %s", url)
}
}
}
func TestHTTPChallengeHandlerSuccess(t *testing.T) {
expectedPath := challengeBasePath + "/asdf"
// Set up fake acme handler backend to make sure proxying succeeds
var proxySuccess bool
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxySuccess = true
if r.URL.Path != expectedPath {
t.Errorf("Expected path '%s' but got '%s' instead", expectedPath, r.URL.Path)
}
}))
// Custom listener that uses the port we expect
ln, err := net.Listen("tcp", "127.0.0.1:"+DefaultHTTPAlternatePort)
if err != nil {
t.Fatalf("Unable to start test server listener: %v", err)
}
ts.Listener = ln
// Start our engines and run the test
ts.Start()
defer ts.Close()
req, err := http.NewRequest("GET", "http://127.0.0.1:"+DefaultHTTPAlternatePort+expectedPath, nil)
if err != nil {
t.Fatalf("Could not craft request, got error: %v", err)
}
rw := httptest.NewRecorder()
HTTPChallengeHandler(rw, req, DefaultHTTPAlternatePort)
if !proxySuccess {
t.Fatal("Expected request to be proxied, but it wasn't")
}
}

228
caddytls/maintain.go Normal file
View File

@ -0,0 +1,228 @@
package caddytls
import (
"log"
"time"
"golang.org/x/crypto/ocsp"
)
func init() {
// maintain assets while this package is imported, which is
// always. we don't ever stop it, since we need it running.
go maintainAssets(make(chan struct{}))
}
const (
// RenewInterval is how often to check certificates for renewal.
RenewInterval = 12 * time.Hour
// OCSPInterval is how often to check if OCSP stapling needs updating.
OCSPInterval = 1 * time.Hour
// RenewDurationBefore is how long before expiration to renew certificates.
RenewDurationBefore = (24 * time.Hour) * 30
)
// maintainAssets is a permanently-blocking function
// that loops indefinitely and, on a regular schedule, checks
// certificates for expiration and initiates a renewal of certs
// that are expiring soon. It also updates OCSP stapling and
// performs other maintenance of assets. It should only be
// called once per process.
//
// You must pass in the channel which you'll close when
// maintenance should stop, to allow this goroutine to clean up
// after itself and unblock. (Not that you HAVE to stop it...)
func maintainAssets(stopChan chan struct{}) {
renewalTicker := time.NewTicker(RenewInterval)
ocspTicker := time.NewTicker(OCSPInterval)
for {
select {
case <-renewalTicker.C:
log.Println("[INFO] Scanning for expiring certificates")
RenewManagedCertificates(false)
log.Println("[INFO] Done checking certificates")
case <-ocspTicker.C:
log.Println("[INFO] Scanning for stale OCSP staples")
UpdateOCSPStaples()
log.Println("[INFO] Done checking OCSP staples")
case <-stopChan:
renewalTicker.Stop()
ocspTicker.Stop()
log.Println("[INFO] Stopped background maintenance routine")
return
}
}
}
// RenewManagedCertificates renews managed certificates.
func RenewManagedCertificates(allowPrompts bool) (err error) {
var renewed, deleted []Certificate
visitedNames := make(map[string]struct{})
certCacheMu.RLock()
for name, cert := range certCache {
if !cert.Config.Managed || cert.Config.SelfSigned {
continue
}
// the list of names on this cert should never be empty...
if cert.Names == nil || len(cert.Names) == 0 {
log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v - removing from cache", name, cert.Names)
deleted = append(deleted, cert)
continue
}
// skip names whose certificate we've already renewed
if _, ok := visitedNames[name]; ok {
continue
}
for _, name := range cert.Names {
visitedNames[name] = struct{}{}
}
// if its time is up or ending soon, we need to try to renew it
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < RenewDurationBefore {
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
if cert.Config == nil {
log.Printf("[ERROR] %s: No associated TLS config; unable to renew", name)
continue
}
// this works well because managed certs are only associated with one name per config
err := cert.Config.RenewCert(allowPrompts)
if err != nil {
if allowPrompts && timeLeft < 0 {
// Certificate renewal failed, the operator is present, and the certificate
// is already expired; we should stop immediately and return the error. Note
// that we used to do this any time a renewal failed at startup. However,
// after discussion in https://github.com/mholt/caddy/issues/642 we decided to
// only stop startup if the certificate is expired. We still log the error
// otherwise.
certCacheMu.RUnlock()
return err
}
log.Printf("[ERROR] %v", err)
if cert.Config.OnDemand {
deleted = append(deleted, cert)
}
} else {
renewed = append(renewed, cert)
}
}
}
certCacheMu.RUnlock()
// Apply changes to the cache
for _, cert := range renewed {
if cert.Names[len(cert.Names)-1] == "" {
// Special case: This is the default certificate. We must
// flush it out of the cache so that we no longer point to
// the old, un-renewed certificate. Otherwise it will be
// renewed on every scan, which is too often. When we cache
// this certificate in a moment, it will be the default again.
certCacheMu.Lock()
delete(certCache, "")
certCacheMu.Unlock()
}
_, err := CacheManagedCertificate(cert.Names[0], cert.Config)
if err != nil {
if allowPrompts {
return err // operator is present, so report error immediately
}
log.Printf("[ERROR] %v", err)
}
}
for _, cert := range deleted {
certCacheMu.Lock()
for _, name := range cert.Names {
delete(certCache, name)
}
certCacheMu.Unlock()
}
return nil
}
// UpdateOCSPStaples updates the OCSP stapling in all
// eligible, cached certificates.
func UpdateOCSPStaples() {
// Create a temporary place to store updates
// until we release the potentially long-lived
// read lock and use a short-lived write lock.
type ocspUpdate struct {
rawBytes []byte
parsed *ocsp.Response
}
updated := make(map[string]ocspUpdate)
// A single SAN certificate maps to multiple names, so we use this
// set to make sure we don't waste cycles checking OCSP for the same
// certificate multiple times.
visited := make(map[string]struct{})
certCacheMu.RLock()
for name, cert := range certCache {
// skip this certificate if we've already visited it,
// and if not, mark all the names as visited
if _, ok := visited[name]; ok {
continue
}
for _, n := range cert.Names {
visited[n] = struct{}{}
}
// no point in updating OCSP for expired certificates
if time.Now().After(cert.NotAfter) {
continue
}
var lastNextUpdate time.Time
if cert.OCSP != nil {
// start checking OCSP staple about halfway through validity period for good measure
lastNextUpdate = cert.OCSP.NextUpdate
refreshTime := cert.OCSP.ThisUpdate.Add(lastNextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
// since OCSP is already stapled, we need only check if we're in that "refresh window"
if time.Now().Before(refreshTime) {
continue
}
}
err := stapleOCSP(&cert, nil)
if err != nil {
if cert.OCSP != nil {
// if there was no staple before, that's fine; otherwise we should log the error
log.Printf("[ERROR] Checking OCSP for %v: %v", cert.Names, err)
}
continue
}
// By this point, we've obtained the latest OCSP response.
// If there was no staple before, or if the response is updated, make
// sure we apply the update to all names on the certificate.
if lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate {
log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s",
cert.Names, lastNextUpdate, cert.OCSP.NextUpdate)
for _, n := range cert.Names {
updated[n] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP}
}
}
}
certCacheMu.RUnlock()
// This write lock should be brief since we have all the info we need now.
certCacheMu.Lock()
for name, update := range updated {
cert := certCache[name]
cert.OCSP = update.parsed
cert.Certificate.OCSPStaple = update.rawBytes
certCache[name] = cert
}
certCacheMu.Unlock()
}

278
caddytls/setup.go Normal file
View File

@ -0,0 +1,278 @@
package caddytls
import (
"bytes"
"crypto/tls"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin(caddy.Plugin{
Name: "tls",
Action: setupTLS,
})
}
// setupTLS sets up the TLS configuration and installs certificates that
// are specified by the user in the config file. All the automatic HTTPS
// stuff comes later outside of this function.
func setupTLS(c *caddy.Controller) error {
configGetter, ok := configGetters[c.ServerType()]
if !ok {
return fmt.Errorf("no caddytls.ConfigGetter for %s server type; must call RegisterConfigGetter", c.ServerType())
}
config := configGetter(c.Key)
if config == nil {
return fmt.Errorf("no caddytls.Config to set up for %s", c.Key)
}
config.Enabled = true
for c.Next() {
var certificateFile, keyFile, loadDir, maxCerts string
args := c.RemainingArgs()
switch len(args) {
case 1:
// even if the email is one of the special values below,
// it is still necessary for future analysis that we store
// that value in the ACMEEmail field.
config.ACMEEmail = args[0]
// user can force-disable managed TLS this way
if args[0] == "off" {
config.Enabled = false
return nil
}
// user might want a temporary, in-memory, self-signed cert
if args[0] == "self_signed" {
config.SelfSigned = true
}
case 2:
certificateFile = args[0]
keyFile = args[1]
config.Manual = true
}
// Optional block with extra parameters
var hadBlock bool
for c.NextBlock() {
hadBlock = true
switch c.Val() {
case "key_type":
arg := c.RemainingArgs()
value, ok := supportedKeyTypes[strings.ToUpper(arg[0])]
if !ok {
return c.Errf("Wrong key type name or key type not supported: '%s'", c.Val())
}
config.KeyType = value
case "protocols":
args := c.RemainingArgs()
if len(args) != 2 {
return c.ArgErr()
}
value, ok := supportedProtocols[strings.ToLower(args[0])]
if !ok {
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
}
config.ProtocolMinVersion = value
value, ok = supportedProtocols[strings.ToLower(args[1])]
if !ok {
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
}
config.ProtocolMaxVersion = value
case "ciphers":
for c.NextArg() {
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
if !ok {
return c.Errf("Wrong cipher name or cipher not supported: '%s'", c.Val())
}
config.Ciphers = append(config.Ciphers, value)
}
case "clients":
clientCertList := c.RemainingArgs()
if len(clientCertList) == 0 {
return c.ArgErr()
}
listStart, mustProvideCA := 1, true
switch clientCertList[0] {
case "request":
config.ClientAuth = tls.RequestClientCert
mustProvideCA = false
case "require":
config.ClientAuth = tls.RequireAnyClientCert
mustProvideCA = false
case "verify_if_given":
config.ClientAuth = tls.VerifyClientCertIfGiven
default:
config.ClientAuth = tls.RequireAndVerifyClientCert
listStart = 0
}
if mustProvideCA && len(clientCertList) <= listStart {
return c.ArgErr()
}
config.ClientCerts = clientCertList[listStart:]
case "load":
c.Args(&loadDir)
config.Manual = true
case "max_certs":
c.Args(&maxCerts)
config.OnDemand = true
case "dns":
args := c.RemainingArgs()
if len(args) != 1 {
return c.ArgErr()
}
dnsProvName := args[0]
if _, ok := dnsProviders[dnsProvName]; !ok {
return c.Errf("Unsupported DNS provider '%s'", args[0])
}
config.DNSProvider = args[0]
default:
return c.Errf("Unknown keyword '%s'", c.Val())
}
}
// tls requires at least one argument if a block is not opened
if len(args) == 0 && !hadBlock {
return c.ArgErr()
}
// set certificate limit if on-demand TLS is enabled
if maxCerts != "" {
maxCertsNum, err := strconv.Atoi(maxCerts)
if err != nil || maxCertsNum < 1 {
return c.Err("max_certs must be a positive integer")
}
if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost...
onDemandMaxIssue = int32(maxCertsNum)
}
}
// don't try to load certificates unless we're supposed to
if !config.Enabled || !config.Manual {
continue
}
// load a single certificate and key, if specified
if certificateFile != "" && keyFile != "" {
err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
if err != nil {
return c.Errf("Unable to load certificate and key files for '%s': %v", c.Key, err)
}
log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile)
}
// load a directory of certificates, if specified
if loadDir != "" {
err := loadCertsInDir(c, loadDir)
if err != nil {
return err
}
}
}
SetDefaultTLSParams(config)
// generate self-signed cert if needed
if config.SelfSigned {
err := makeSelfSignedCert(config)
if err != nil {
return fmt.Errorf("self-signed: %v", err)
}
}
return nil
}
// loadCertsInDir loads all the certificates/keys in dir, as long as
// the file ends with .pem. This method of loading certificates is
// modeled after haproxy, which expects the certificate and key to
// be bundled into the same file:
// https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt
//
// This function may write to the log as it walks the directory tree.
func loadCertsInDir(c *caddy.Controller, dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("[WARNING] Unable to traverse into %s; skipping", path)
return nil
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
var foundKey bool // use only the first key in the file
bundle, err := ioutil.ReadFile(path)
if err != nil {
return err
}
for {
// Decode next block so we can see what type it is
var derBlock *pem.Block
derBlock, bundle = pem.Decode(bundle)
if derBlock == nil {
break
}
if derBlock.Type == "CERTIFICATE" {
// Re-encode certificate as PEM, appending to certificate chain
pem.Encode(certBuilder, derBlock)
} else if derBlock.Type == "EC PARAMETERS" {
// EC keys generated from openssl can be composed of two blocks:
// parameters and key (parameter block should come first)
if !foundKey {
// Encode parameters
pem.Encode(keyBuilder, derBlock)
// Key must immediately follow
derBlock, bundle = pem.Decode(bundle)
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path)
}
pem.Encode(keyBuilder, derBlock)
foundKey = true
}
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
// RSA key
if !foundKey {
pem.Encode(keyBuilder, derBlock)
foundKey = true
}
} else {
return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type)
}
}
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
if len(certPEMBytes) == 0 {
return c.Errf("%s: failed to parse PEM data", path)
}
if len(keyPEMBytes) == 0 {
return c.Errf("%s: no private key block found", path)
}
err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
if err != nil {
return c.Errf("%s: failed to load cert and key for '%s': %v", path, c.Key, err)
}
log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
}
return nil
})
}

298
caddytls/setup_test.go Normal file
View File

@ -0,0 +1,298 @@
package caddytls
import (
"crypto/tls"
"io/ioutil"
"log"
"os"
"testing"
"github.com/mholt/caddy"
"github.com/xenolf/lego/acme"
)
func TestMain(m *testing.M) {
// Write test certificates to disk before tests, and clean up
// when we're done.
err := ioutil.WriteFile(certFile, testCert, 0644)
if err != nil {
log.Fatal(err)
}
err = ioutil.WriteFile(keyFile, testKey, 0644)
if err != nil {
os.Remove(certFile)
log.Fatal(err)
}
result := m.Run()
os.Remove(certFile)
os.Remove(keyFile)
os.Exit(result)
}
func TestSetupParseBasic(t *testing.T) {
cfg := new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c := caddy.NewTestController(`tls ` + certFile + ` ` + keyFile + ``)
err := setupTLS(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
// Basic checks
if !cfg.Manual {
t.Error("Expected TLS Manual=true, but was false")
}
if !cfg.Enabled {
t.Error("Expected TLS Enabled=true, but was false")
}
// Security defaults
if cfg.ProtocolMinVersion != tls.VersionTLS11 {
t.Errorf("Expected 'tls1.1 (0x0302)' as ProtocolMinVersion, got %#v", cfg.ProtocolMinVersion)
}
if cfg.ProtocolMaxVersion != tls.VersionTLS12 {
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", cfg.ProtocolMaxVersion)
}
// Cipher checks
expectedCiphers := []uint16{
tls.TLS_FALLBACK_SCSV,
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_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,
}
// Ensure count is correct (plus one for TLS_FALLBACK_SCSV)
if len(cfg.Ciphers) != len(expectedCiphers) {
t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v",
len(expectedCiphers), len(cfg.Ciphers))
}
// Ensure ordering is correct
for i, actual := range cfg.Ciphers {
if actual != expectedCiphers[i] {
t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
}
}
if !cfg.PreferServerCipherSuites {
t.Error("Expected PreferServerCipherSuites = true, but was false")
}
}
func TestSetupParseIncompleteParams(t *testing.T) {
// Using tls without args is an error because it's unnecessary.
c := caddy.NewTestController(`tls`)
err := setupTLS(c)
if err == nil {
t.Error("Expected an error, but didn't get one")
}
}
func TestSetupParseWithOptionalParams(t *testing.T) {
params := `tls ` + certFile + ` ` + keyFile + ` {
protocols tls1.0 tls1.2
ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384
}`
cfg := new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c := caddy.NewTestController(params)
err := setupTLS(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if cfg.ProtocolMinVersion != tls.VersionTLS10 {
t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", cfg.ProtocolMinVersion)
}
if cfg.ProtocolMaxVersion != tls.VersionTLS12 {
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %#v", cfg.ProtocolMaxVersion)
}
if len(cfg.Ciphers)-1 != 3 {
t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(cfg.Ciphers)-1)
}
}
func TestSetupDefaultWithOptionalParams(t *testing.T) {
params := `tls {
ciphers RSA-3DES-EDE-CBC-SHA
}`
cfg := new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c := caddy.NewTestController(params)
err := setupTLS(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if len(cfg.Ciphers)-1 != 1 {
t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(cfg.Ciphers)-1)
}
}
func TestSetupParseWithWrongOptionalParams(t *testing.T) {
// Test protocols wrong params
params := `tls ` + certFile + ` ` + keyFile + ` {
protocols ssl tls
}`
cfg := new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c := caddy.NewTestController(params)
err := setupTLS(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
// Test ciphers wrong params
params = `tls ` + certFile + ` ` + keyFile + ` {
ciphers not-valid-cipher
}`
cfg = new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c = caddy.NewTestController(params)
err = setupTLS(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
// Test key_type wrong params
params = `tls {
key_type ab123
}`
cfg = new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c = caddy.NewTestController(params)
err = setupTLS(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
}
func TestSetupParseWithClientAuth(t *testing.T) {
// Test missing client cert file
params := `tls ` + certFile + ` ` + keyFile + ` {
clients
}`
cfg := new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c := caddy.NewTestController(params)
err := setupTLS(c)
if err == nil {
t.Errorf("Expected an error, but no error returned")
}
noCAs, twoCAs := []string{}, []string{"client_ca.crt", "client2_ca.crt"}
for caseNumber, caseData := range []struct {
params string
clientAuthType tls.ClientAuthType
expectedErr bool
expectedCAs []string
}{
{"", tls.NoClientCert, false, noCAs},
{`tls ` + certFile + ` ` + keyFile + ` {
clients client_ca.crt client2_ca.crt
}`, tls.RequireAndVerifyClientCert, false, twoCAs},
// now come modifier
{`tls ` + certFile + ` ` + keyFile + ` {
clients request
}`, tls.RequestClientCert, false, noCAs},
{`tls ` + certFile + ` ` + keyFile + ` {
clients require
}`, tls.RequireAnyClientCert, false, noCAs},
{`tls ` + certFile + ` ` + keyFile + ` {
clients verify_if_given client_ca.crt client2_ca.crt
}`, tls.VerifyClientCertIfGiven, false, twoCAs},
{`tls ` + certFile + ` ` + keyFile + ` {
clients verify_if_given
}`, tls.VerifyClientCertIfGiven, true, noCAs},
} {
cfg := new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c := caddy.NewTestController(caseData.params)
err := setupTLS(c)
if caseData.expectedErr {
if err == nil {
t.Errorf("In case %d: Expected an error, got: %v", caseNumber, err)
}
continue
}
if err != nil {
t.Errorf("In case %d: Expected no errors, got: %v", caseNumber, err)
}
if caseData.clientAuthType != cfg.ClientAuth {
t.Errorf("In case %d: Expected TLS client auth type %v, got: %v",
caseNumber, caseData.clientAuthType, cfg.ClientAuth)
}
if count := len(cfg.ClientCerts); count < len(caseData.expectedCAs) {
t.Fatalf("In case %d: Expected %d client certs, had %d", caseNumber, len(caseData.expectedCAs), count)
}
for idx, expected := range caseData.expectedCAs {
if actual := cfg.ClientCerts[idx]; actual != expected {
t.Errorf("In case %d: Expected %dth client cert file to be '%s', but was '%s'",
caseNumber, idx, expected, actual)
}
}
}
}
func TestSetupParseWithKeyType(t *testing.T) {
params := `tls {
key_type p384
}`
cfg := new(Config)
RegisterConfigGetter("", func(key string) *Config { return cfg })
c := caddy.NewTestController(params)
err := setupTLS(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if cfg.KeyType != acme.EC384 {
t.Errorf("Expected 'P384' as KeyType, got %#v", cfg.KeyType)
}
}
const (
certFile = "test_cert.pem"
keyFile = "test_key.pem"
)
var testCert = []byte(`-----BEGIN CERTIFICATE-----
MIIBkjCCATmgAwIBAgIJANfFCBcABL6LMAkGByqGSM49BAEwFDESMBAGA1UEAxMJ
bG9jYWxob3N0MB4XDTE2MDIxMDIyMjAyNFoXDTE4MDIwOTIyMjAyNFowFDESMBAG
A1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs22MtnG7
9K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDLSiVQvFZ6lUszTlczNxVk
pEfqrM6xAupB7qN1MHMwHQYDVR0OBBYEFHxYDvAxUwL4XrjPev6qZ/BiLDs5MEQG
A1UdIwQ9MDuAFHxYDvAxUwL4XrjPev6qZ/BiLDs5oRikFjAUMRIwEAYDVQQDEwls
b2NhbGhvc3SCCQDXxQgXAAS+izAMBgNVHRMEBTADAQH/MAkGByqGSM49BAEDSAAw
RQIgRvBqbyJM2JCJqhA1FmcoZjeMocmhxQHTt1c+1N2wFUgCIQDtvrivbBPA688N
Qh3sMeAKNKPsx5NxYdoWuu9KWcKz9A==
-----END CERTIFICATE-----
`)
var testKey = []byte(`-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGLtRmwzYVcrH3J0BnzYbGPdWVF10i9p6mxkA4+b2fURoAoGCCqGSM49
AwEHoUQDQgAEs22MtnG79K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDL
SiVQvFZ6lUszTlczNxVkpEfqrM6xAupB7g==
-----END EC PRIVATE KEY-----
`)

134
caddytls/storage.go Normal file
View File

@ -0,0 +1,134 @@
package caddytls
import (
"fmt"
"net/url"
"path/filepath"
"strings"
"github.com/mholt/caddy"
)
// StorageFor gets the storage value associated with the
// caURL, which should be unique for every different
// ACME CA.
func StorageFor(caURL string) (Storage, error) {
if caURL == "" {
caURL = DefaultCAUrl
}
if caURL == "" {
return "", 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 "", fmt.Errorf("%s: unable to parse CA URL: %v", caURL, err)
}
if u.Host == "" {
return "", fmt.Errorf("%s: no host in CA URL", caURL)
}
return Storage(filepath.Join(storageBasePath, u.Host)), nil
}
// Storage is a root directory and facilitates
// forming file paths derived from it. It is used
// to get file paths in a consistent, cross-
// platform way for persisting ACME assets.
// on the file system.
type Storage string
// Sites gets the directory that stores site certificate and keys.
func (s Storage) Sites() string {
return filepath.Join(string(s), "sites")
}
// Site returns the path to the folder containing assets for domain.
func (s Storage) Site(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.Sites(), domain)
}
// SiteCertFile returns the path to the certificate file for domain.
func (s Storage) SiteCertFile(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.Site(domain), domain+".crt")
}
// SiteKeyFile returns the path to domain's private key file.
func (s Storage) SiteKeyFile(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.Site(domain), domain+".key")
}
// SiteMetaFile returns the path to the domain's asset metadata file.
func (s Storage) SiteMetaFile(domain string) string {
domain = strings.ToLower(domain)
return filepath.Join(s.Site(domain), domain+".json")
}
// Users gets the directory that stores account folders.
func (s Storage) Users() string {
return filepath.Join(string(s), "users")
}
// User gets the account folder for the user with email.
func (s Storage) User(email string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
return filepath.Join(s.Users(), email)
}
// UserRegFile gets the path to the registration file for
// the user with the given email address.
func (s Storage) UserRegFile(email string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
fileName := emailUsername(email)
if fileName == "" {
fileName = "registration"
}
return filepath.Join(s.User(email), fileName+".json")
}
// UserKeyFile gets the path to the private key file for
// the user with the given email address.
func (s Storage) UserKeyFile(email string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
fileName := emailUsername(email)
if fileName == "" {
fileName = "private"
}
return filepath.Join(s.User(email), fileName+".key")
}
// emailUsername returns the username portion of an
// email address (part before '@') or the original
// input if it can't find the "@" symbol.
func emailUsername(email string) string {
at := strings.Index(email, "@")
if at == -1 {
return email
} else if at == 0 {
return email[1:]
}
return email[:at]
}
// storageBasePath is the root path in which all TLS/ACME assets are
// stored. Do not change this value during the lifetime of the program.
var storageBasePath = filepath.Join(caddy.AssetsPath(), "acme")

135
caddytls/storage_test.go Normal file
View File

@ -0,0 +1,135 @@
package caddytls
import (
"path/filepath"
"testing"
)
func TestStorageFor(t *testing.T) {
// first try without DefaultCAUrl set
DefaultCAUrl = ""
_, err := StorageFor("")
if err == nil {
t.Errorf("Without a default CA, expected error, but didn't get one")
}
st, err := StorageFor("https://example.com/foo")
if err != nil {
t.Errorf("Without a default CA but given input, expected no error, but got: %v", err)
}
if string(st) != filepath.Join(storageBasePath, "example.com") {
t.Errorf("Without a default CA but given input, expected '%s' not '%s'", "example.com", st)
}
// try with the DefaultCAUrl set
DefaultCAUrl = "https://defaultCA/directory"
for i, test := range []struct {
input, expect string
shouldErr bool
}{
{"https://acme-staging.api.letsencrypt.org/directory", "acme-staging.api.letsencrypt.org", false},
{"https://foo/boo?bar=q", "foo", false},
{"http://foo", "foo", false},
{"", "defaultca", false},
{"https://FooBar/asdf", "foobar", false},
{"noscheme/path", "noscheme", false},
{"/nohost", "", true},
{"https:///nohost", "", true},
{"FooBar", "foobar", false},
} {
st, err := StorageFor(test.input)
if err == nil && test.shouldErr {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d: Expected no errors, but got: %v", i, err)
}
want := filepath.Join(storageBasePath, test.expect)
if test.shouldErr {
want = ""
}
if string(st) != want {
t.Errorf("Test %d: Expected '%s' but got '%s'", i, want, string(st))
}
}
}
func TestStorage(t *testing.T) {
storage := Storage("./le_test")
if expected, actual := filepath.Join("le_test", "sites"), storage.Sites(); actual != expected {
t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("Test.com"); actual != expected {
t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("Test.com"); actual != expected {
t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected {
t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("TEST.COM"); actual != expected {
t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users"), storage.Users(); actual != expected {
t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("Me@example.com"); actual != expected {
t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("ME@EXAMPLE.COM"); actual != expected {
t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected {
t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual)
}
// Test with empty emails
if expected, actual := filepath.Join("le_test", "users", emptyEmail), storage.User(emptyEmail); actual != expected {
t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected {
t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected {
t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual)
}
}
func TestEmailUsername(t *testing.T) {
for i, test := range []struct {
input, expect string
}{
{
input: "username@example.com",
expect: "username",
},
{
input: "plus+addressing@example.com",
expect: "plus+addressing",
},
{
input: "me+plus-addressing@example.com",
expect: "me+plus-addressing",
},
{
input: "not-an-email",
expect: "not-an-email",
},
{
input: "@foobar.com",
expect: "foobar.com",
},
{
input: emptyEmail,
expect: emptyEmail,
},
{
input: "",
expect: "",
},
} {
if actual := emailUsername(test.input); actual != test.expect {
t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual)
}
}
}

187
caddytls/tls.go Normal file
View File

@ -0,0 +1,187 @@
// 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.
package caddytls
import (
"encoding/json"
"io/ioutil"
"net"
"os"
"strings"
"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.
func HostQualifies(hostname string) bool {
return hostname != "localhost" && // localhost is ineligible
// hostname must not be empty
strings.TrimSpace(hostname) != "" &&
// must not contain wildcard (*) characters (until CA supports it)
!strings.Contains(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
}
// existingCertAndKey returns true if the hostname has
// a certificate and private key in storage already under
// the storage provided, otherwise it returns false.
func existingCertAndKey(storage Storage, hostname string) bool {
_, err := os.Stat(storage.SiteCertFile(hostname))
if err != nil {
return false
}
_, err = os.Stat(storage.SiteKeyFile(hostname))
if err != nil {
return false
}
return true
}
// 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 {
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
if err != nil {
return err
}
// Save cert
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
if err != nil {
return err
}
// Save private key
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
if err != nil {
return err
}
// Save cert metadata
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
if err != nil {
return err
}
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
if err != nil {
return err
}
return nil
}
// 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)
}
// tlsSniSolver is a type that can solve tls-sni challenges using
// an existing listener and our custom, in-memory certificate cache.
type tlsSniSolver struct{}
// Present adds the challenge certificate to the cache.
func (s tlsSniSolver) Present(domain, token, keyAuth string) error {
cert, err := acme.TLSSNI01ChallengeCert(keyAuth)
if err != nil {
return err
}
cacheCertificate(Certificate{
Certificate: cert,
Names: []string{domain},
})
return nil
}
// CleanUp removes the challenge certificate from the cache.
func (s tlsSniSolver) CleanUp(domain, token, keyAuth string) error {
uncacheCertificate(domain)
return nil
}
// 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)
}
// DNSProviderConstructor is a function that takes credentials and
// returns a type that can solve the ACME DNS challenges.
type DNSProviderConstructor func(credentials ...string) (acme.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
}
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
)

165
caddytls/tls_test.go Normal file
View File

@ -0,0 +1,165 @@
package caddytls
import (
"io/ioutil"
"os"
"testing"
"github.com/xenolf/lego/acme"
)
func TestHostQualifies(t *testing.T) {
for i, test := range []struct {
host string
expect bool
}{
{"example.com", true},
{"sub.example.com", true},
{"Sub.Example.COM", true},
{"127.0.0.1", false},
{"127.0.1.5", false},
{"69.123.43.94", false},
{"::1", false},
{"::", false},
{"0.0.0.0", false},
{"", false},
{" ", false},
{"*.example.com", false},
{".com", false},
{"example.com.", false},
{"localhost", false},
{"local", true},
{"devsite", true},
{"192.168.1.3", false},
{"10.0.2.1", false},
{"169.112.53.4", false},
} {
actual := HostQualifies(test.host)
if actual != test.expect {
t.Errorf("Test %d: Expected HostQualifies(%s)=%v, but got %v",
i, test.host, test.expect, actual)
}
}
}
type holder struct {
host, port string
cfg *Config
}
func (h holder) TLSConfig() *Config { return h.cfg }
func (h holder) Host() string { return h.host }
func (h holder) Port() string { return h.port }
func TestQualifiesForManagedTLS(t *testing.T) {
for i, test := range []struct {
cfg ConfigHolder
expect bool
}{
{holder{host: ""}, false},
{holder{host: "localhost"}, false},
{holder{host: "123.44.3.21"}, false},
{holder{host: "example.com"}, false},
{holder{host: "", cfg: new(Config)}, false},
{holder{host: "localhost", cfg: new(Config)}, false},
{holder{host: "123.44.3.21", cfg: new(Config)}, false},
{holder{host: "example.com", cfg: new(Config)}, true},
{holder{host: "*.example.com", cfg: new(Config)}, false},
{holder{host: "example.com", cfg: &Config{Manual: true}}, false},
{holder{host: "example.com", cfg: &Config{ACMEEmail: "off"}}, false},
{holder{host: "example.com", cfg: &Config{ACMEEmail: "foo@bar.com"}}, true},
{holder{host: "example.com", port: "80"}, false},
{holder{host: "example.com", port: "1234", cfg: new(Config)}, true},
{holder{host: "example.com", port: "443", cfg: new(Config)}, true},
{holder{host: "example.com", port: "80"}, false},
} {
if got, want := QualifiesForManagedTLS(test.cfg), test.expect; got != want {
t.Errorf("Test %d: Expected %v but got %v", i, want, got)
}
}
}
func TestSaveCertResource(t *testing.T) {
storage := Storage("./le_test_save")
defer func() {
err := os.RemoveAll(string(storage))
if err != nil {
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
}
}()
domain := "example.com"
certContents := "certificate"
keyContents := "private key"
metaContents := `{
"domain": "example.com",
"certUrl": "https://example.com/cert",
"certStableUrl": "https://example.com/cert/stable"
}`
cert := acme.CertificateResource{
Domain: domain,
CertURL: "https://example.com/cert",
CertStableURL: "https://example.com/cert/stable",
PrivateKey: []byte(keyContents),
Certificate: []byte(certContents),
}
err := saveCertResource(storage, cert)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain))
if err != nil {
t.Errorf("Expected no error reading certificate file, got: %v", err)
}
if string(certFile) != certContents {
t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile))
}
keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain))
if err != nil {
t.Errorf("Expected no error reading private key file, got: %v", err)
}
if string(keyFile) != keyContents {
t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile))
}
metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain))
if err != nil {
t.Errorf("Expected no error reading meta file, got: %v", err)
}
if string(metaFile) != metaContents {
t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile))
}
}
func TestExistingCertAndKey(t *testing.T) {
storage := Storage("./le_test_existing")
defer func() {
err := os.RemoveAll(string(storage))
if err != nil {
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
}
}()
domain := "example.com"
if existingCertAndKey(storage, domain) {
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
}
err := saveCertResource(storage, acme.CertificateResource{
Domain: domain,
PrivateKey: []byte("key"),
Certificate: []byte("cert"),
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if !existingCertAndKey(storage, domain) {
t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
}
}

200
caddytls/user.go Normal file
View File

@ -0,0 +1,200 @@
package caddytls
import (
"bufio"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/xenolf/lego/acme"
)
// User represents a Let's Encrypt user account.
type User struct {
Email string
Registration *acme.RegistrationResource
key crypto.PrivateKey
}
// GetEmail gets u's email.
func (u User) GetEmail() string {
return u.Email
}
// GetRegistration gets u's registration resource.
func (u User) GetRegistration() *acme.RegistrationResource {
return u.Registration
}
// GetPrivateKey gets u's private key.
func (u User) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// newUser creates a new User for the given email address
// with a new private key. This function does NOT save the
// user to disk or register it via ACME. If you want to use
// a user account that might already exist, call getUser
// instead. It does NOT prompt the user.
func newUser(email string) (User, error) {
user := User{Email: email}
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return user, errors.New("error generating private key: " + err.Error())
}
user.key = privateKey
return user, nil
}
// getEmail does everything it can to obtain an email
// address from the user within the scope of storage
// to use for ACME TLS. If it cannot get an email
// address, it returns empty string. (It will warn the
// user of the consequences of an empty email.) This
// function MAY prompt the user for input. If userPresent
// is false, the operator will NOT be prompted and an
// empty email may be returned.
func getEmail(storage Storage, userPresent bool) string {
// First try memory (command line flag or typed by user previously)
leEmail := DefaultEmail
if leEmail == "" {
// Then try to get most recent user email
userDirs, err := ioutil.ReadDir(storage.Users())
if err == nil {
var mostRecent os.FileInfo
for _, dir := range userDirs {
if !dir.IsDir() {
continue
}
if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) {
leEmail = dir.Name()
DefaultEmail = leEmail // save for next time
mostRecent = dir
}
}
}
}
if leEmail == "" && userPresent {
// Alas, we must bother the user and ask for an email address;
// if they proceed they also agree to the SA.
reader := bufio.NewReader(stdin)
fmt.Println("\nYour sites will be served over HTTPS automatically using Let's Encrypt.")
fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:")
fmt.Println(" " + saURL) // TODO: Show current SA link
fmt.Println("Please enter your email address so you can recover your account if needed.")
fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.")
fmt.Print("Email address: ")
var err error
leEmail, err = reader.ReadString('\n')
if err != nil {
return ""
}
leEmail = strings.TrimSpace(leEmail)
DefaultEmail = leEmail
Agreed = true
}
return strings.ToLower(leEmail)
}
// getUser loads the user with the given email from disk
// using the provided storage. If the user does not exist,
// it will create a new one, but it does NOT save new
// users to the disk or register them via ACME. It does
// NOT prompt the user.
func getUser(storage Storage, email string) (User, error) {
var user User
// open user file
regFile, err := os.Open(storage.UserRegFile(email))
if err != nil {
if os.IsNotExist(err) {
// create a new user
return newUser(email)
}
return user, err
}
defer regFile.Close()
// load user information
err = json.NewDecoder(regFile).Decode(&user)
if err != nil {
return user, err
}
// load their private key
user.key, err = loadPrivateKey(storage.UserKeyFile(email))
if err != nil {
return user, err
}
return user, nil
}
// saveUser persists a user's key and account registration
// to the file system. It does NOT register the user via ACME
// or prompt the user. You must also pass in the storage
// wherein the user should be saved. It should be the storage
// for the CA with which user has an account.
func saveUser(storage Storage, user User) error {
// make user account folder
err := os.MkdirAll(storage.User(user.Email), 0700)
if err != nil {
return err
}
// save private key file
err = savePrivateKey(user.key, storage.UserKeyFile(user.Email))
if err != nil {
return err
}
// save registration file
jsonBytes, err := json.MarshalIndent(&user, "", "\t")
if err != nil {
return err
}
return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600)
}
// promptUserAgreement prompts the user to agree to the agreement
// at agreementURL via stdin. If the agreement has changed, then pass
// true as the second argument. If this is the user's first time
// agreeing, pass false. It returns whether the user agreed or not.
func promptUserAgreement(agreementURL string, changed bool) bool {
if changed {
fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL)
fmt.Print("Do you agree to the new terms? (y/n): ")
} else {
fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL)
fmt.Print("Do you agree to the terms? (y/n): ")
}
reader := bufio.NewReader(stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false
}
answer = strings.ToLower(strings.TrimSpace(answer))
return answer == "y" || answer == "yes"
}
// stdin is used to read the user's input if prompted;
// this is changed by tests during tests.
var stdin = io.ReadWriter(os.Stdin)
// The name of the folder for accounts where the email
// address was not provided; default 'username' if you will.
const emptyEmail = "default"
// TODO: Use latest
const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"

184
caddytls/user_test.go Normal file
View File

@ -0,0 +1,184 @@
package caddytls
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"io"
"os"
"strings"
"testing"
"time"
"github.com/xenolf/lego/acme"
)
func TestUser(t *testing.T) {
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
if err != nil {
t.Fatalf("Could not generate test private key: %v", err)
}
u := User{
Email: "me@mine.com",
Registration: new(acme.RegistrationResource),
key: privateKey,
}
if expected, actual := "me@mine.com", u.GetEmail(); actual != expected {
t.Errorf("Expected email '%s' but got '%s'", expected, actual)
}
if u.GetRegistration() == nil {
t.Error("Expected a registration resource, but got nil")
}
if expected, actual := privateKey, u.GetPrivateKey(); actual != expected {
t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual)
}
}
func TestNewUser(t *testing.T) {
email := "me@foobar.com"
user, err := newUser(email)
if err != nil {
t.Fatalf("Error creating user: %v", err)
}
if user.key == nil {
t.Error("Private key is nil")
}
if user.Email != email {
t.Errorf("Expected email to be %s, but was %s", email, user.Email)
}
if user.Registration != nil {
t.Error("New user already has a registration resource; it shouldn't")
}
}
func TestSaveUser(t *testing.T) {
defer os.RemoveAll(string(testStorage))
email := "me@foobar.com"
user, err := newUser(email)
if err != nil {
t.Fatalf("Error creating user: %v", err)
}
err = saveUser(testStorage, user)
if err != nil {
t.Fatalf("Error saving user: %v", err)
}
_, err = os.Stat(testStorage.UserRegFile(email))
if err != nil {
t.Errorf("Cannot access user registration file, error: %v", err)
}
_, err = os.Stat(testStorage.UserKeyFile(email))
if err != nil {
t.Errorf("Cannot access user private key file, error: %v", err)
}
}
func TestGetUserDoesNotAlreadyExist(t *testing.T) {
defer os.RemoveAll(string(testStorage))
user, err := getUser(testStorage, "user_does_not_exist@foobar.com")
if err != nil {
t.Fatalf("Error getting user: %v", err)
}
if user.key == nil {
t.Error("Expected user to have a private key, but it was nil")
}
}
func TestGetUserAlreadyExists(t *testing.T) {
defer os.RemoveAll(string(testStorage))
email := "me@foobar.com"
// Set up test
user, err := newUser(email)
if err != nil {
t.Fatalf("Error creating user: %v", err)
}
err = saveUser(testStorage, user)
if err != nil {
t.Fatalf("Error saving user: %v", err)
}
// Expect to load user from disk
user2, err := getUser(testStorage, email)
if err != nil {
t.Fatalf("Error getting user: %v", err)
}
// Assert keys are the same
if !PrivateKeysSame(user.key, user2.key) {
t.Error("Expected private key to be the same after loading, but it wasn't")
}
// Assert emails are the same
if user.Email != user2.Email {
t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email)
}
}
func TestGetEmail(t *testing.T) {
storageBasePath = string(testStorage) // to contain calls that create a new Storage...
// let's not clutter up the output
origStdout := os.Stdout
os.Stdout = nil
defer func() { os.Stdout = origStdout }()
defer os.RemoveAll(string(testStorage))
DefaultEmail = "test2@foo.com"
// Test1: Use default email from flag (or user previously typing it)
actual := getEmail(testStorage, true)
if actual != DefaultEmail {
t.Errorf("Did not get correct email from memory; expected '%s' but got '%s'", DefaultEmail, actual)
}
// Test2: Get input from user
DefaultEmail = ""
stdin = new(bytes.Buffer)
_, err := io.Copy(stdin, strings.NewReader("test3@foo.com\n"))
if err != nil {
t.Fatalf("Could not simulate user input, error: %v", err)
}
actual = getEmail(testStorage, true)
if actual != "test3@foo.com" {
t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual)
}
// Test3: Get most recent email from before
DefaultEmail = ""
for i, eml := range []string{
"TEST4-3@foo.com", // test case insensitivity
"test4-2@foo.com",
"test4-1@foo.com",
} {
u, err := newUser(eml)
if err != nil {
t.Fatalf("Error creating user %d: %v", i, err)
}
err = saveUser(testStorage, u)
if err != nil {
t.Fatalf("Error saving user %d: %v", i, err)
}
// Change modified time so they're all different, so the test becomes deterministic
f, err := os.Stat(testStorage.User(eml))
if err != nil {
t.Fatalf("Could not access user folder for '%s': %v", eml, err)
}
chTime := f.ModTime().Add(-(time.Duration(i) * time.Second))
if err := os.Chtimes(testStorage.User(eml), chTime, chTime); err != nil {
t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err)
}
}
actual = getEmail(testStorage, true)
if actual != "test4-3@foo.com" {
t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual)
}
}
var testStorage = Storage("./testdata")