16 changed files with 1278 additions and 118 deletions
@ -0,0 +1,84 @@ |
|||||
|
package caddytls |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/go-acme/lego/certcrypto" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
"github.com/go-acme/lego/challenge" |
||||
|
"github.com/mholt/certmagic" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.management.acme", |
||||
|
New: func() (interface{}, error) { return new(acmeManagerMaker), nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// ManagerMaker TODO: WIP...
|
||||
|
type ManagerMaker interface { |
||||
|
newManager(interactive bool) (certmagic.Manager, error) |
||||
|
} |
||||
|
|
||||
|
// acmeManagerMaker makes an ACME manager
|
||||
|
// for managinig certificates using ACME.
|
||||
|
type acmeManagerMaker struct { |
||||
|
CA string `json:"ca,omitempty"` |
||||
|
Email string `json:"email,omitempty"` |
||||
|
RenewAhead caddy2.Duration `json:"renew_ahead,omitempty"` |
||||
|
KeyType string `json:"key_type,omitempty"` |
||||
|
ACMETimeout caddy2.Duration `json:"acme_timeout,omitempty"` |
||||
|
MustStaple bool `json:"must_staple,omitempty"` |
||||
|
Challenges ChallengesConfig `json:"challenges"` |
||||
|
OnDemand *OnDemandConfig `json:"on_demand,omitempty"` |
||||
|
Storage json.RawMessage `json:"storage,omitempty"` |
||||
|
|
||||
|
storage certmagic.Storage |
||||
|
keyType certcrypto.KeyType |
||||
|
} |
||||
|
|
||||
|
func (m *acmeManagerMaker) Provision() error { |
||||
|
m.setDefaults() |
||||
|
|
||||
|
// DNS providers
|
||||
|
if m.Challenges.DNS != nil { |
||||
|
val, err := caddy2.LoadModuleInline("provider", "tls.dns", m.Challenges.DNS) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading TLS storage module: %s", err) |
||||
|
} |
||||
|
m.Challenges.dns = val.(challenge.Provider) |
||||
|
m.Challenges.DNS = nil // allow GC to deallocate - TODO: Does this help?
|
||||
|
} |
||||
|
|
||||
|
// policy-specific storage implementation
|
||||
|
if m.Storage != nil { |
||||
|
val, err := caddy2.LoadModuleInline("system", "caddy.storage", m.Storage) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading TLS storage module: %s", err) |
||||
|
} |
||||
|
cmStorage, err := val.(caddy2.StorageConverter).CertMagicStorage() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("creating TLS storage configuration: %v", err) |
||||
|
} |
||||
|
m.storage = cmStorage |
||||
|
m.Storage = nil // allow GC to deallocate - TODO: Does this help?
|
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// setDefaults indiscriminately sets all the default values in m.
|
||||
|
func (m *acmeManagerMaker) setDefaults() { |
||||
|
m.CA = certmagic.LetsEncryptStagingCA // certmagic.Default.CA // TODO: When not testing, switch to production CA
|
||||
|
m.Email = certmagic.Default.Email |
||||
|
m.RenewAhead = caddy2.Duration(certmagic.Default.RenewDurationBefore) |
||||
|
m.keyType = certmagic.Default.KeyType |
||||
|
m.storage = certmagic.Default.Storage |
||||
|
} |
||||
|
|
||||
|
func (m *acmeManagerMaker) newManager(interactive bool) (certmagic.Manager, error) { |
||||
|
return nil, nil |
||||
|
} |
@ -0,0 +1,149 @@ |
|||||
|
package caddytls |
||||
|
|
||||
|
import ( |
||||
|
"crypto/tls" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
"github.com/go-acme/lego/challenge/tlsalpn01" |
||||
|
"github.com/mholt/certmagic" |
||||
|
) |
||||
|
|
||||
|
// ConnectionPolicies is an ordered group of connection policies;
|
||||
|
// the first matching policy will be used to configure TLS
|
||||
|
// connections at handshake-time.
|
||||
|
type ConnectionPolicies []*ConnectionPolicy |
||||
|
|
||||
|
// TLSConfig converts the group of policies to a standard-lib-compatible
|
||||
|
// TLS configuration which selects the first matching policy based on
|
||||
|
// the ClientHello.
|
||||
|
func (cp ConnectionPolicies) TLSConfig(handle caddy2.Handle) (*tls.Config, error) { |
||||
|
// connection policy matchers
|
||||
|
for i, pol := range cp { |
||||
|
for modName, rawMsg := range pol.MatchersRaw { |
||||
|
val, err := caddy2.LoadModule("tls.handshake_match."+modName, rawMsg) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("loading handshake matcher module '%s': %s", modName, err) |
||||
|
} |
||||
|
cp[i].Matchers = append(cp[i].Matchers, val.(ConnectionMatcher)) |
||||
|
} |
||||
|
cp[i].MatchersRaw = nil // allow GC to deallocate - TODO: Does this help?
|
||||
|
} |
||||
|
|
||||
|
// pre-build standard TLS configs so we don't have to at handshake-time
|
||||
|
for i := range cp { |
||||
|
err := cp[i].buildStandardTLSConfig(handle) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return &tls.Config{ |
||||
|
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { |
||||
|
policyLoop: |
||||
|
for _, pol := range cp { |
||||
|
for _, matcher := range pol.Matchers { |
||||
|
if !matcher.Match(hello) { |
||||
|
continue policyLoop |
||||
|
} |
||||
|
} |
||||
|
return pol.stdTLSConfig, nil |
||||
|
} |
||||
|
return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) |
||||
|
}, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
// ConnectionPolicy specifies the logic for handling a TLS handshake.
|
||||
|
type ConnectionPolicy struct { |
||||
|
MatchersRaw map[string]json.RawMessage `json:"match,omitempty"` |
||||
|
|
||||
|
CipherSuites []string `json:"cipher_suites,omitempty"` |
||||
|
Curves []string `json:"curves,omitempty"` |
||||
|
ALPN []string `json:"alpn,omitempty"` |
||||
|
ProtocolMin string `json:"protocol_min,omitempty"` |
||||
|
ProtocolMax string `json:"protocol_max,omitempty"` |
||||
|
|
||||
|
// TODO: Client auth
|
||||
|
|
||||
|
// TODO: see if starlark could be useful here - enterprise only
|
||||
|
StarlarkHandshake string `json:"starlark_handshake,omitempty"` |
||||
|
|
||||
|
Matchers []ConnectionMatcher |
||||
|
stdTLSConfig *tls.Config |
||||
|
} |
||||
|
|
||||
|
func (cp *ConnectionPolicy) buildStandardTLSConfig(handle caddy2.Handle) error { |
||||
|
tlsApp := handle.App("tls").(*TLS) |
||||
|
|
||||
|
cfg := &tls.Config{ |
||||
|
NextProtos: cp.ALPN, |
||||
|
PreferServerCipherSuites: true, |
||||
|
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { |
||||
|
cfgTpl, err := tlsApp.getConfigForName(hello.ServerName) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err) |
||||
|
} |
||||
|
newCfg := certmagic.New(tlsApp.certCache, cfgTpl) |
||||
|
return newCfg.GetCertificate(hello) |
||||
|
}, |
||||
|
MinVersion: tls.VersionTLS12, |
||||
|
MaxVersion: tls.VersionTLS13, |
||||
|
// TODO: Session ticket key rotation (use Storage)
|
||||
|
} |
||||
|
|
||||
|
// add all the cipher suites in order, without duplicates
|
||||
|
cipherSuitesAdded := make(map[uint16]struct{}) |
||||
|
for _, csName := range cp.CipherSuites { |
||||
|
csID := supportedCipherSuites[csName] |
||||
|
if _, ok := cipherSuitesAdded[csID]; !ok { |
||||
|
cipherSuitesAdded[csID] = struct{}{} |
||||
|
cfg.CipherSuites = append(cfg.CipherSuites, csID) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// add all the curve preferences in order, without duplicates
|
||||
|
curvesAdded := make(map[tls.CurveID]struct{}) |
||||
|
for _, curveName := range cp.Curves { |
||||
|
curveID := supportedCurves[curveName] |
||||
|
if _, ok := curvesAdded[curveID]; !ok { |
||||
|
curvesAdded[curveID] = struct{}{} |
||||
|
cfg.CurvePreferences = append(cfg.CurvePreferences, curveID) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ensure ALPN includes the ACME TLS-ALPN protocol
|
||||
|
var alpnFound bool |
||||
|
for _, a := range cp.ALPN { |
||||
|
if a == tlsalpn01.ACMETLS1Protocol { |
||||
|
alpnFound = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if !alpnFound { |
||||
|
cfg.NextProtos = append(cfg.NextProtos, tlsalpn01.ACMETLS1Protocol) |
||||
|
} |
||||
|
|
||||
|
// min and max protocol versions
|
||||
|
if cp.ProtocolMin != "" { |
||||
|
cfg.MinVersion = supportedProtocols[cp.ProtocolMin] |
||||
|
} |
||||
|
if cp.ProtocolMax != "" { |
||||
|
cfg.MaxVersion = supportedProtocols[cp.ProtocolMax] |
||||
|
} |
||||
|
if cp.ProtocolMin > cp.ProtocolMax { |
||||
|
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", cp.ProtocolMin, cp.ProtocolMax) |
||||
|
} |
||||
|
|
||||
|
// TODO: client auth, and other fields
|
||||
|
|
||||
|
cp.stdTLSConfig = cfg |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// ConnectionMatcher is a type which matches TLS handshakes.
|
||||
|
type ConnectionMatcher interface { |
||||
|
Match(*tls.ClientHelloInfo) bool |
||||
|
} |
@ -0,0 +1,61 @@ |
|||||
|
package caddytls |
||||
|
|
||||
|
import ( |
||||
|
"crypto/tls" |
||||
|
"fmt" |
||||
|
"io/ioutil" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.certificates.load_files", |
||||
|
New: func() (interface{}, error) { return fileLoader{}, nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// fileLoader loads certificates and their associated keys from disk.
|
||||
|
type fileLoader []CertKeyFilePair |
||||
|
|
||||
|
// CertKeyFilePair pairs certificate and key file names along with their
|
||||
|
// encoding format so that they can be loaded from disk.
|
||||
|
type CertKeyFilePair struct { |
||||
|
Certificate string `json:"certificate"` |
||||
|
Key string `json:"key"` |
||||
|
Format string `json:"format,omitempty"` // "pem" is default
|
||||
|
} |
||||
|
|
||||
|
// LoadCertificates returns the certificates to be loaded by fl.
|
||||
|
func (fl fileLoader) LoadCertificates() ([]tls.Certificate, error) { |
||||
|
var certs []tls.Certificate |
||||
|
for _, pair := range fl { |
||||
|
certData, err := ioutil.ReadFile(pair.Certificate) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
keyData, err := ioutil.ReadFile(pair.Key) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
var cert tls.Certificate |
||||
|
switch pair.Format { |
||||
|
case "": |
||||
|
fallthrough |
||||
|
case "pem": |
||||
|
cert, err = tls.X509KeyPair(certData, keyData) |
||||
|
default: |
||||
|
return nil, fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format) |
||||
|
} |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
certs = append(certs, cert) |
||||
|
} |
||||
|
return certs, nil |
||||
|
} |
||||
|
|
||||
|
// Interface guard
|
||||
|
var _ CertificateLoader = (fileLoader)(nil) |
@ -0,0 +1,122 @@ |
|||||
|
package caddytls |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"crypto/tls" |
||||
|
"encoding/pem" |
||||
|
"fmt" |
||||
|
"io/ioutil" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.certificates.load_folders", |
||||
|
New: func() (interface{}, error) { return folderLoader{}, nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// folderLoader loads certificates and their associated keys from disk
|
||||
|
// by recursively walking the specified directories, looking for PEM
|
||||
|
// files which contain both a certificate and a key.
|
||||
|
type folderLoader []string |
||||
|
|
||||
|
// LoadCertificates loads all the certificates+keys in the directories
|
||||
|
// listed in fl from all files ending with .pem. This method of loading
|
||||
|
// certificates expects the certificate and key to be bundled into the
|
||||
|
// same file.
|
||||
|
func (fl folderLoader) LoadCertificates() ([]tls.Certificate, error) { |
||||
|
var certs []tls.Certificate |
||||
|
for _, dir := range fl { |
||||
|
err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error { |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("unable to traverse into path: %s", fpath) |
||||
|
} |
||||
|
if info.IsDir() { |
||||
|
return nil |
||||
|
} |
||||
|
if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
cert, err := x509CertFromCertAndKeyPEMFile(fpath) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
certs = append(certs, cert) |
||||
|
|
||||
|
return nil |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
} |
||||
|
return certs, nil |
||||
|
} |
||||
|
|
||||
|
func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) { |
||||
|
bundle, err := ioutil.ReadFile(fpath) |
||||
|
if err != nil { |
||||
|
return tls.Certificate{}, err |
||||
|
} |
||||
|
|
||||
|
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer) |
||||
|
var foundKey bool // use only the first key in the file
|
||||
|
|
||||
|
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 tls.Certificate{}, fmt.Errorf("%s: expected elliptic private key to immediately follow EC parameters", fpath) |
||||
|
} |
||||
|
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 tls.Certificate{}, fmt.Errorf("%s: unrecognized PEM block type: %s", fpath, derBlock.Type) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes() |
||||
|
if len(certPEMBytes) == 0 { |
||||
|
return tls.Certificate{}, fmt.Errorf("%s: failed to parse PEM data", fpath) |
||||
|
} |
||||
|
if len(keyPEMBytes) == 0 { |
||||
|
return tls.Certificate{}, fmt.Errorf("%s: no private key block found", fpath) |
||||
|
} |
||||
|
|
||||
|
cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes) |
||||
|
if err != nil { |
||||
|
return tls.Certificate{}, fmt.Errorf("%s: making X509 key pair: %v", fpath, err) |
||||
|
} |
||||
|
|
||||
|
return cert, nil |
||||
|
} |
@ -0,0 +1,79 @@ |
|||||
|
package caddytls |
||||
|
|
||||
|
import ( |
||||
|
"crypto/tls" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
) |
||||
|
|
||||
|
type ( |
||||
|
MatchServerName []string |
||||
|
|
||||
|
// TODO: these others should be enterprise-only, probably
|
||||
|
MatchProtocol []string // TODO: version or protocol?
|
||||
|
MatchClientCert struct{} // TODO: client certificate options
|
||||
|
MatchRemote []string |
||||
|
MatchStarlark string |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.handshake_match.host", |
||||
|
New: func() (interface{}, error) { return MatchServerName{}, nil }, |
||||
|
}) |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.handshake_match.protocol", |
||||
|
New: func() (interface{}, error) { return MatchProtocol{}, nil }, |
||||
|
}) |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.handshake_match.client_cert", |
||||
|
New: func() (interface{}, error) { return MatchClientCert{}, nil }, |
||||
|
}) |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.handshake_match.remote", |
||||
|
New: func() (interface{}, error) { return MatchRemote{}, nil }, |
||||
|
}) |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls.handshake_match.starlark", |
||||
|
New: func() (interface{}, error) { return new(MatchStarlark), nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { |
||||
|
for _, name := range m { |
||||
|
// TODO: support wildcards (and regex?)
|
||||
|
if hello.ServerName == name { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (m MatchProtocol) Match(hello *tls.ClientHelloInfo) bool { |
||||
|
// TODO: not implemented
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (m MatchClientCert) Match(hello *tls.ClientHelloInfo) bool { |
||||
|
// TODO: not implemented
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (m MatchRemote) Match(hello *tls.ClientHelloInfo) bool { |
||||
|
// TODO: not implemented
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (m MatchStarlark) Match(hello *tls.ClientHelloInfo) bool { |
||||
|
// TODO: not implemented
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// Interface guards
|
||||
|
var ( |
||||
|
_ ConnectionMatcher = MatchServerName{} |
||||
|
_ ConnectionMatcher = MatchProtocol{} |
||||
|
_ ConnectionMatcher = MatchClientCert{} |
||||
|
_ ConnectionMatcher = MatchRemote{} |
||||
|
_ ConnectionMatcher = new(MatchStarlark) |
||||
|
) |
@ -0,0 +1,359 @@ |
|||||
|
package caddytls |
||||
|
|
||||
|
import ( |
||||
|
"crypto/tls" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
"time" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
"github.com/go-acme/lego/certcrypto" |
||||
|
"github.com/go-acme/lego/challenge" |
||||
|
"github.com/klauspost/cpuid" |
||||
|
"github.com/mholt/certmagic" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "tls", |
||||
|
New: func() (interface{}, error) { return new(TLS), nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TLS represents a process-wide TLS configuration.
|
||||
|
type TLS struct { |
||||
|
Certificates map[string]json.RawMessage `json:"certificates"` |
||||
|
Automation AutomationConfig `json:"automation"` |
||||
|
|
||||
|
certificateLoaders []CertificateLoader |
||||
|
certCache *certmagic.Cache |
||||
|
} |
||||
|
|
||||
|
// TODO: Finish stubbing out this two-phase setup process: prepare, then start...
|
||||
|
|
||||
|
func (t *TLS) Provision() error { |
||||
|
// set up the certificate cache
|
||||
|
// TODO: this makes a new cache every time; better to only make a new
|
||||
|
// cache (or even better, add/remove only what is necessary) if the
|
||||
|
// certificates config has been updated
|
||||
|
t.certCache = certmagic.NewCache(certmagic.CacheOptions{ |
||||
|
GetConfigForCert: func(cert certmagic.Certificate) (certmagic.Config, error) { |
||||
|
return t.getConfigForName(cert.Names[0]) |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
for i, ap := range t.Automation.Policies { |
||||
|
val, err := caddy2.LoadModuleInline("module", "tls.management", ap.Management) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading TLS automation management module: %s", err) |
||||
|
} |
||||
|
t.Automation.Policies[i].management = val.(ManagerMaker) |
||||
|
t.Automation.Policies[i].Management = nil // allow GC to deallocate - TODO: Does this help?
|
||||
|
} |
||||
|
|
||||
|
// certificate loaders
|
||||
|
for modName, rawMsg := range t.Certificates { |
||||
|
if modName == automateKey { |
||||
|
continue // special case; these will be loaded in later
|
||||
|
} |
||||
|
val, err := caddy2.LoadModule("tls.certificates."+modName, rawMsg) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading certificate module '%s': %s", modName, err) |
||||
|
} |
||||
|
t.certificateLoaders = append(t.certificateLoaders, val.(CertificateLoader)) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Start activates the TLS module.
|
||||
|
func (t *TLS) Start(handle caddy2.Handle) error { |
||||
|
// load manual/static (unmanaged) certificates
|
||||
|
for _, loader := range t.certificateLoaders { |
||||
|
certs, err := loader.LoadCertificates() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("loading certificates: %v", err) |
||||
|
} |
||||
|
magic := certmagic.New(t.certCache, certmagic.Config{ |
||||
|
Storage: caddy2.GetStorage(), |
||||
|
}) |
||||
|
for _, cert := range certs { |
||||
|
err := magic.CacheUnmanagedTLSCertificate(cert) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("caching unmanaged certificate: %v", err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// load automated (managed) certificates
|
||||
|
if automatedRawMsg, ok := t.Certificates[automateKey]; ok { |
||||
|
var names []string |
||||
|
err := json.Unmarshal(automatedRawMsg, &names) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("automate: decoding names: %v", err) |
||||
|
} |
||||
|
err = t.Manage(names) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("automate: managing %v: %v", names, err) |
||||
|
} |
||||
|
// for _, name := range names {
|
||||
|
// t.Manage([]string{name)
|
||||
|
// ap := t.getAutomationPolicyForName(name)
|
||||
|
// magic := certmagic.New(t.certCache, ap.makeCertMagicConfig())
|
||||
|
// err := magic.Manage([]string{name})
|
||||
|
// if err != nil {
|
||||
|
// return fmt.Errorf("automate: manage %s: %v", name, err)
|
||||
|
// }
|
||||
|
// }
|
||||
|
} |
||||
|
t.Certificates = nil // allow GC to deallocate - TODO: Does this help?
|
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Stop stops the TLS module and cleans up any allocations.
|
||||
|
func (t *TLS) Stop() error { |
||||
|
if t.certCache != nil { |
||||
|
// TODO: ensure locks are cleaned up too... maybe in certmagic though
|
||||
|
t.certCache.Stop() |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Manage immediately begins managing names according to the
|
||||
|
// matching automation policy.
|
||||
|
func (t *TLS) Manage(names []string) error { |
||||
|
for _, name := range names { |
||||
|
ap := t.getAutomationPolicyForName(name) |
||||
|
magic := certmagic.New(t.certCache, ap.makeCertMagicConfig()) |
||||
|
err := magic.Manage([]string{name}) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("automate: manage %s: %v", name, err) |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// HandleHTTPChallenge ensures that the HTTP challenge is handled for the
|
||||
|
// certificate named by r.Host, if it is an HTTP challenge request.
|
||||
|
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { |
||||
|
if !certmagic.LooksLikeHTTPChallenge(r) { |
||||
|
return false |
||||
|
} |
||||
|
ap := t.getAutomationPolicyForName(r.Host) |
||||
|
magic := certmagic.New(t.certCache, ap.makeCertMagicConfig()) |
||||
|
return magic.HandleHTTPChallenge(w, r) |
||||
|
} |
||||
|
|
||||
|
func (t *TLS) getConfigForName(name string) (certmagic.Config, error) { |
||||
|
ap := t.getAutomationPolicyForName(name) |
||||
|
return ap.makeCertMagicConfig(), nil |
||||
|
} |
||||
|
|
||||
|
func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy { |
||||
|
for _, ap := range t.Automation.Policies { |
||||
|
if len(ap.Hosts) == 0 { |
||||
|
// no host filter is an automatic match
|
||||
|
return ap |
||||
|
} |
||||
|
for _, h := range ap.Hosts { |
||||
|
if h == name { |
||||
|
return ap |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// default automation policy
|
||||
|
mgmt := new(acmeManagerMaker) |
||||
|
mgmt.setDefaults() |
||||
|
return AutomationPolicy{management: mgmt} |
||||
|
} |
||||
|
|
||||
|
// CertificateLoader is a type that can load certificates.
|
||||
|
type CertificateLoader interface { |
||||
|
LoadCertificates() ([]tls.Certificate, error) |
||||
|
} |
||||
|
|
||||
|
// AutomationConfig designates configuration for the
|
||||
|
// construction and use of ACME clients.
|
||||
|
type AutomationConfig struct { |
||||
|
Policies []AutomationPolicy `json:"policies,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// AutomationPolicy designates the policy for automating the
|
||||
|
// management of managed TLS certificates.
|
||||
|
type AutomationPolicy struct { |
||||
|
Hosts []string `json:"hosts,omitempty"` |
||||
|
Management json.RawMessage `json:"management"` |
||||
|
|
||||
|
management ManagerMaker |
||||
|
} |
||||
|
|
||||
|
func (ap AutomationPolicy) makeCertMagicConfig() certmagic.Config { |
||||
|
if acmeMgmt, ok := ap.management.(*acmeManagerMaker); ok { |
||||
|
// default, which is management via ACME
|
||||
|
|
||||
|
storage := acmeMgmt.storage |
||||
|
if storage == nil { |
||||
|
storage = caddy2.GetStorage() |
||||
|
} |
||||
|
|
||||
|
var ond *certmagic.OnDemandConfig |
||||
|
if acmeMgmt.OnDemand != nil { |
||||
|
ond = &certmagic.OnDemandConfig{ |
||||
|
// TODO: fill this out
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return certmagic.Config{ |
||||
|
CA: certmagic.LetsEncryptStagingCA, //ap.CA, // TODO: Restore true value
|
||||
|
Email: acmeMgmt.Email, |
||||
|
Agreed: true, |
||||
|
DisableHTTPChallenge: acmeMgmt.Challenges.HTTP.Disabled, |
||||
|
DisableTLSALPNChallenge: acmeMgmt.Challenges.TLSALPN.Disabled, |
||||
|
RenewDurationBefore: time.Duration(acmeMgmt.RenewAhead), |
||||
|
AltHTTPPort: acmeMgmt.Challenges.HTTP.AlternatePort, |
||||
|
AltTLSALPNPort: acmeMgmt.Challenges.TLSALPN.AlternatePort, |
||||
|
DNSProvider: acmeMgmt.Challenges.dns, |
||||
|
KeyType: supportedCertKeyTypes[acmeMgmt.KeyType], |
||||
|
CertObtainTimeout: time.Duration(acmeMgmt.ACMETimeout), |
||||
|
OnDemand: ond, |
||||
|
MustStaple: acmeMgmt.MustStaple, |
||||
|
Storage: storage, |
||||
|
// TODO: listenHost
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return certmagic.Config{ |
||||
|
NewManager: ap.management.newManager, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ChallengesConfig configures the ACME challenges.
|
||||
|
type ChallengesConfig struct { |
||||
|
HTTP HTTPChallengeConfig `json:"http"` |
||||
|
TLSALPN TLSALPNChallengeConfig `json:"tls-alpn"` |
||||
|
DNS json.RawMessage `json:"dns,omitempty"` |
||||
|
|
||||
|
dns challenge.Provider |
||||
|
} |
||||
|
|
||||
|
// HTTPChallengeConfig configures the ACME HTTP challenge.
|
||||
|
type HTTPChallengeConfig struct { |
||||
|
Disabled bool `json:"disabled,omitempty"` |
||||
|
AlternatePort int `json:"alternate_port,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// TLSALPNChallengeConfig configures the ACME TLS-ALPN challenge.
|
||||
|
type TLSALPNChallengeConfig struct { |
||||
|
Disabled bool `json:"disabled,omitempty"` |
||||
|
AlternatePort int `json:"alternate_port,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// OnDemandConfig configures on-demand TLS, for obtaining
|
||||
|
// needed certificates at handshake-time.
|
||||
|
type OnDemandConfig struct { |
||||
|
// TODO: MaxCertificates state might not endure reloads...
|
||||
|
// MaxCertificates int `json:"max_certificates,omitempty"`
|
||||
|
AskURL string `json:"ask_url,omitempty"` |
||||
|
AskStarlark string `json:"ask_starlark,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// supportedCertKeyTypes is all the key types that are supported
|
||||
|
// for certificates that are obtained through ACME.
|
||||
|
var supportedCertKeyTypes = map[string]certcrypto.KeyType{ |
||||
|
"RSA2048": certcrypto.RSA2048, |
||||
|
"RSA4096": certcrypto.RSA4096, |
||||
|
"P256": certcrypto.EC256, |
||||
|
"P384": certcrypto.EC384, |
||||
|
} |
||||
|
|
||||
|
// supportedCipherSuites is the unordered map of cipher suite
|
||||
|
// string names to their definition in crypto/tls.
|
||||
|
// TODO: might not be needed much longer, see:
|
||||
|
// https://github.com/golang/go/issues/30325
|
||||
|
var supportedCipherSuites = map[string]uint16{ |
||||
|
"ECDHE_ECDSA_AES256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, |
||||
|
"ECDHE_RSA_AES256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |
||||
|
"ECDHE_ECDSA_AES128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |
||||
|
"ECDHE_RSA_AES128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |
||||
|
"ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, |
||||
|
"ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, |
||||
|
"ECDHE_RSA_AES256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, |
||||
|
"ECDHE_RSA_AES128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_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_AES256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, |
||||
|
"RSA_AES128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_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, |
||||
|
} |
||||
|
|
||||
|
// defaultCipherSuites is the ordered list of all the cipher
|
||||
|
// suites we want to support by default, assuming AES-NI
|
||||
|
// (hardware acceleration for AES).
|
||||
|
var defaultCipherSuitesWithAESNI = []uint16{ |
||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, |
||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |
||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |
||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |
||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, |
||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, |
||||
|
} |
||||
|
|
||||
|
// defaultCipherSuites is the ordered list of all the cipher
|
||||
|
// suites we want to support by default, assuming lack of
|
||||
|
// AES-NI (NO hardware acceleration for AES).
|
||||
|
var defaultCipherSuitesWithoutAESNI = []uint16{ |
||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, |
||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, |
||||
|
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, |
||||
|
} |
||||
|
|
||||
|
// getOptimalDefaultCipherSuites returns an appropriate cipher
|
||||
|
// suite to use depending on the hardware support for AES.
|
||||
|
//
|
||||
|
// See https://github.com/mholt/caddy/issues/1674
|
||||
|
func getOptimalDefaultCipherSuites() []uint16 { |
||||
|
if cpuid.CPU.AesNi() { |
||||
|
return defaultCipherSuitesWithAESNI |
||||
|
} |
||||
|
return defaultCipherSuitesWithoutAESNI |
||||
|
} |
||||
|
|
||||
|
// supportedCurves is the unordered map of supported curves.
|
||||
|
// https://golang.org/pkg/crypto/tls/#CurveID
|
||||
|
var supportedCurves = map[string]tls.CurveID{ |
||||
|
"X25519": tls.X25519, |
||||
|
"P256": tls.CurveP256, |
||||
|
"P384": tls.CurveP384, |
||||
|
"P521": tls.CurveP521, |
||||
|
} |
||||
|
|
||||
|
// defaultCurves is the list of only the curves we want to use
|
||||
|
// by default, in descending order of preference.
|
||||
|
//
|
||||
|
// This list should only include curves which are fast by design
|
||||
|
// (e.g. X25519) and those for which an optimized assembly
|
||||
|
// implementation exists (e.g. P256). The latter ones can be
|
||||
|
// found here:
|
||||
|
// https://github.com/golang/go/tree/master/src/crypto/elliptic
|
||||
|
var defaultCurves = []tls.CurveID{ |
||||
|
tls.X25519, |
||||
|
tls.CurveP256, |
||||
|
} |
||||
|
|
||||
|
// supportedProtocols is a 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, |
||||
|
"tls1.3": tls.VersionTLS13, |
||||
|
} |
||||
|
|
||||
|
const automateKey = "automate" |
@ -0,0 +1,74 @@ |
|||||
|
package caddy2 |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"runtime" |
||||
|
|
||||
|
"github.com/mholt/certmagic" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
RegisterModule(Module{ |
||||
|
Name: "caddy.storage.file_system", |
||||
|
New: func() (interface{}, error) { return new(fileStorage), nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// StorageConverter is a type that can convert itself
|
||||
|
// to a valid, usable certmagic.Storage value. The
|
||||
|
// value might be short-lived.
|
||||
|
type StorageConverter interface { |
||||
|
CertMagicStorage() (certmagic.Storage, error) |
||||
|
} |
||||
|
|
||||
|
// TODO: Wrappers other than file_system should be enterprise-only.
|
||||
|
|
||||
|
// It may seem trivial to wrap these, but the benefits are:
|
||||
|
// 1. We don't need to change the actual CertMagic storage implementions
|
||||
|
// to a structure that is operable with Caddy's config (including JSON
|
||||
|
// tags), and
|
||||
|
// 2. We don't need to rely on rely on maintainers of third-party
|
||||
|
// certmagic.Storage implementations. We can make any certmagic.Storage
|
||||
|
// work with Caddy this way.
|
||||
|
|
||||
|
// fileStorage is a certmagic.Storage wrapper for certmagic.FileStorage.
|
||||
|
type fileStorage struct { |
||||
|
Root string `json:"root"` |
||||
|
} |
||||
|
|
||||
|
func (s fileStorage) CertMagicStorage() (certmagic.Storage, error) { |
||||
|
return &certmagic.FileStorage{Path: s.Root}, nil |
||||
|
} |
||||
|
|
||||
|
// homeDir returns the best guess of the current user's home
|
||||
|
// directory from environment variables. If unknown, "." (the
|
||||
|
// current directory) is returned instead.
|
||||
|
func homeDir() string { |
||||
|
home := os.Getenv("HOME") |
||||
|
if home == "" && runtime.GOOS == "windows" { |
||||
|
drive := os.Getenv("HOMEDRIVE") |
||||
|
path := os.Getenv("HOMEPATH") |
||||
|
home = drive + path |
||||
|
if drive == "" || path == "" { |
||||
|
home = os.Getenv("USERPROFILE") |
||||
|
} |
||||
|
} |
||||
|
if home == "" { |
||||
|
home = "." |
||||
|
} |
||||
|
return home |
||||
|
} |
||||
|
|
||||
|
// dataDir returns a directory path that is suitable for storage.
|
||||
|
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables
|
||||
|
func dataDir() string { |
||||
|
baseDir := filepath.Join(homeDir(), ".local", "share") |
||||
|
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { |
||||
|
baseDir = xdgData |
||||
|
} |
||||
|
return filepath.Join(baseDir, "caddy") |
||||
|
} |
||||
|
|
||||
|
// Interface guard
|
||||
|
var _ StorageConverter = fileStorage{} |
Loading…
Reference in new issue