mirror of
https://github.com/caddyserver/caddy.git
synced 2025-06-03 18:53:27 +08:00
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:
237
caddytls/certificates.go
Normal file
237
caddytls/certificates.go
Normal 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()
|
||||
}
|
59
caddytls/certificates_test.go
Normal file
59
caddytls/certificates_test.go
Normal 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
298
caddytls/client.go
Normal 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
3
caddytls/client_test.go
Normal file
@ -0,0 +1,3 @@
|
||||
package caddytls
|
||||
|
||||
// TODO
|
437
caddytls/config.go
Normal file
437
caddytls/config.go
Normal 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
258
caddytls/crypto.go
Normal 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
166
caddytls/crypto_test.go
Normal 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
312
caddytls/handshake.go
Normal 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")
|
56
caddytls/handshake_test.go
Normal file
56
caddytls/handshake_test.go
Normal 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
42
caddytls/httphandler.go
Normal 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
|
||||
}
|
63
caddytls/httphandler_test.go
Normal file
63
caddytls/httphandler_test.go
Normal 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
228
caddytls/maintain.go
Normal 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
278
caddytls/setup.go
Normal 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
298
caddytls/setup_test.go
Normal 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
134
caddytls/storage.go
Normal 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
135
caddytls/storage_test.go
Normal 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
187
caddytls/tls.go
Normal 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
165
caddytls/tls_test.go
Normal 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
200
caddytls/user.go
Normal 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
184
caddytls/user_test.go
Normal 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")
|
Reference in New Issue
Block a user