summaryrefslogtreecommitdiff
path: root/modules/caddytls
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2019-10-10 11:27:45 -0600
committerGitHub <noreply@github.com>2019-10-10 11:27:45 -0600
commitb38365ff3b6ab8b2c474603fa0603831e7eff2a7 (patch)
tree7a35ea901e4ce8d3fbe028e8fc81bd1f69c6be68 /modules/caddytls
parent53dd600b4de0c2b169f1eb4450a0f02950912524 (diff)
parent26cc8837084f9cea6057e9908f0b5bde0eb15d3e (diff)
Merge pull request #2799 from caddyserver/v2-enterprise-merge
v2: Merge enterprise code into open source v2 branch
Diffstat (limited to 'modules/caddytls')
-rw-r--r--modules/caddytls/certselection.go71
-rw-r--r--modules/caddytls/distributedstek/distributedstek.go228
-rw-r--r--modules/caddytls/pemloader.go65
3 files changed, 364 insertions, 0 deletions
diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go
new file mode 100644
index 0000000..b56185a
--- /dev/null
+++ b/modules/caddytls/certselection.go
@@ -0,0 +1,71 @@
+package caddytls
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "math/big"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/mholt/certmagic"
+)
+
+func init() {
+ caddy.RegisterModule(Policy{})
+}
+
+// Policy represents a policy for selecting the certificate used to
+// complete a handshake when there may be multiple options. All fields
+// specified must match the candidate certificate for it to be chosen.
+// This was needed to solve https://github.com/caddyserver/caddy/issues/2588.
+type Policy struct {
+ SerialNumber *big.Int `json:"serial_number,omitempty"`
+ SubjectOrganization string `json:"subject_organization,omitempty"`
+ PublicKeyAlgorithm PublicKeyAlgorithm `json:"public_key_algorithm,omitempty"`
+ Tag string `json:"tag,omitempty"`
+}
+
+// CaddyModule returns the Caddy module information.
+func (Policy) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "tls.certificate_selection.custom",
+ New: func() caddy.Module { return new(Policy) },
+ }
+}
+
+// SelectCertificate implements certmagic.CertificateSelector.
+func (p Policy) SelectCertificate(_ *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) {
+ for _, cert := range choices {
+ if p.SerialNumber != nil && cert.SerialNumber.Cmp(p.SerialNumber) != 0 {
+ continue
+ }
+
+ if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
+ PublicKeyAlgorithm(cert.PublicKeyAlgorithm) != p.PublicKeyAlgorithm {
+ continue
+ }
+
+ if p.SubjectOrganization != "" {
+ var matchOrg bool
+ for _, org := range cert.Subject.Organization {
+ if p.SubjectOrganization == org {
+ matchOrg = true
+ break
+ }
+ }
+ if !matchOrg {
+ continue
+ }
+ }
+
+ if p.Tag != "" && !cert.HasTag(p.Tag) {
+ continue
+ }
+
+ return cert, nil
+ }
+ return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
+}
+
+// Interface guard
+var _ certmagic.CertificateSelector = (*Policy)(nil)
diff --git a/modules/caddytls/distributedstek/distributedstek.go b/modules/caddytls/distributedstek/distributedstek.go
new file mode 100644
index 0000000..a0c4cd2
--- /dev/null
+++ b/modules/caddytls/distributedstek/distributedstek.go
@@ -0,0 +1,228 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package distributedstek provides TLS session ticket ephemeral
+// keys (STEKs) in a distributed fashion by utilizing configured
+// storage for locking and key sharing. This allows a cluster of
+// machines to optimally resume TLS sessions in a load-balanced
+// environment without any hassle. This is similar to what
+// Twitter does, but without needing to rely on SSH, as it is
+// built into the web server this way:
+// https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
+package distributedstek
+
+import (
+ "bytes"
+ "encoding/gob"
+ "encoding/json"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
+ "github.com/mholt/certmagic"
+)
+
+func init() {
+ caddy.RegisterModule(Provider{})
+}
+
+// Provider implements a distributed STEK provider.
+type Provider struct {
+ Storage json.RawMessage `json:"storage,omitempty"`
+
+ storage certmagic.Storage
+ stekConfig *caddytls.SessionTicketService
+ timer *time.Timer
+}
+
+// CaddyModule returns the Caddy module information.
+func (Provider) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "tls.stek.distributed",
+ New: func() caddy.Module { return new(Provider) },
+ }
+}
+
+// Provision provisions s.
+func (s *Provider) Provision(ctx caddy.Context) error {
+ // unpack the storage module to use, if different from the default
+ if s.Storage != nil {
+ val, err := ctx.LoadModuleInline("module", "caddy.storage", s.Storage)
+ if err != nil {
+ return fmt.Errorf("loading TLS storage module: %s", err)
+ }
+ cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
+ if err != nil {
+ return fmt.Errorf("creating TLS storage configuration: %v", err)
+ }
+ s.storage = cmStorage
+ s.Storage = nil // allow GC to deallocate
+ }
+
+ // otherwise, use default storage
+ if s.storage == nil {
+ s.storage = ctx.Storage()
+ }
+
+ return nil
+}
+
+// Initialize sets the configuration for s and returns the starting keys.
+func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
+ // keep a reference to the config; we'll need it when rotating keys
+ s.stekConfig = config
+
+ dstek, err := s.getSTEK()
+ if err != nil {
+ return nil, err
+ }
+
+ // create timer for the remaining time on the interval;
+ // this timer is cleaned up only when rotate() returns
+ s.timer = time.NewTimer(time.Until(dstek.NextRotation))
+
+ return dstek.Keys, nil
+}
+
+// Next returns a channel which transmits the latest session ticket keys.
+func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
+ keysChan := make(chan [][32]byte)
+ go s.rotate(doneChan, keysChan)
+ return keysChan
+}
+
+func (s *Provider) loadSTEK() (distributedSTEK, error) {
+ var sg distributedSTEK
+ gobBytes, err := s.storage.Load(stekFileName)
+ if err != nil {
+ return sg, err // don't wrap, in case error is certmagic.ErrNotExist
+ }
+ dec := gob.NewDecoder(bytes.NewReader(gobBytes))
+ err = dec.Decode(&sg)
+ if err != nil {
+ return sg, fmt.Errorf("STEK gob corrupted: %v", err)
+ }
+ return sg, nil
+}
+
+func (s *Provider) storeSTEK(dstek distributedSTEK) error {
+ var buf bytes.Buffer
+ err := gob.NewEncoder(&buf).Encode(dstek)
+ if err != nil {
+ return fmt.Errorf("encoding STEK gob: %v", err)
+ }
+ err = s.storage.Store(stekFileName, buf.Bytes())
+ if err != nil {
+ return fmt.Errorf("storing STEK gob: %v", err)
+ }
+ return nil
+}
+
+// getSTEK locks and loads the current STEK from storage. If none
+// currently exists, a new STEK is created and persisted. If the
+// current STEK is outdated (NextRotation time is in the past),
+// then it is rotated and persisted. The resulting STEK is returned.
+func (s *Provider) getSTEK() (distributedSTEK, error) {
+ s.storage.Lock(stekLockName)
+ defer s.storage.Unlock(stekLockName)
+
+ // load the current STEKs from storage
+ dstek, err := s.loadSTEK()
+ if _, isNotExist := err.(certmagic.ErrNotExist); isNotExist {
+ // if there is none, then make some right away
+ dstek, err = s.rotateKeys(dstek)
+ if err != nil {
+ return dstek, fmt.Errorf("creating new STEK: %v", err)
+ }
+ } else if err != nil {
+ // some other error, that's a problem
+ return dstek, fmt.Errorf("loading STEK: %v", err)
+ } else if time.Now().After(dstek.NextRotation) {
+ // if current STEKs are outdated, rotate them
+ dstek, err = s.rotateKeys(dstek)
+ if err != nil {
+ return dstek, fmt.Errorf("rotating keys: %v", err)
+ }
+ }
+
+ return dstek, nil
+}
+
+// rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
+// with updated keys and timestamps. It stores the returned STEK in storage,
+// so this function must only be called in a storage-provided lock.
+func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
+ var newSTEK distributedSTEK
+ var err error
+
+ newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
+ if err != nil {
+ return newSTEK, err
+ }
+
+ now := time.Now()
+ newSTEK.LastRotation = now
+ newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
+
+ err = s.storeSTEK(newSTEK)
+ if err != nil {
+ return newSTEK, err
+ }
+
+ return newSTEK, nil
+}
+
+// rotate rotates keys on a regular basis, sending each updated set of
+// keys down keysChan, until doneChan is closed.
+func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
+ for {
+ select {
+ case <-s.timer.C:
+ dstek, err := s.getSTEK()
+ if err != nil {
+ // TODO: improve this handling
+ log.Printf("[ERROR] Loading STEK: %v", err)
+ continue
+ }
+
+ // send the updated keys to the service
+ keysChan <- dstek.Keys
+
+ // timer channel is already drained, so reset directly (see godoc)
+ s.timer.Reset(time.Until(dstek.NextRotation))
+
+ case <-doneChan:
+ // again, see godocs for why timer is stopped this way
+ if !s.timer.Stop() {
+ <-s.timer.C
+ }
+ return
+ }
+ }
+}
+
+type distributedSTEK struct {
+ Keys [][32]byte
+ LastRotation, NextRotation time.Time
+}
+
+const (
+ stekLockName = "stek_check"
+ stekFileName = "stek/stek.bin"
+)
+
+// Interface guard
+var _ caddytls.STEKProvider = (*Provider)(nil)
diff --git a/modules/caddytls/pemloader.go b/modules/caddytls/pemloader.go
new file mode 100644
index 0000000..30a491c
--- /dev/null
+++ b/modules/caddytls/pemloader.go
@@ -0,0 +1,65 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddytls
+
+import (
+ "crypto/tls"
+ "fmt"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(PEMLoader{})
+}
+
+// PEMLoader loads certificates and their associated keys by
+// decoding their PEM blocks directly. This has the advantage
+// of not needing to store them on disk at all.
+type PEMLoader []CertKeyPEMPair
+
+// CaddyModule returns the Caddy module information.
+func (PEMLoader) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "tls.certificates.load_pem",
+ New: func() caddy.Module { return PEMLoader{} },
+ }
+}
+
+// CertKeyPEMPair pairs certificate and key PEM blocks.
+type CertKeyPEMPair struct {
+ CertificatePEM string `json:"certificate"`
+ KeyPEM string `json:"key"`
+ Tags []string `json:"tags,omitempty"`
+}
+
+// LoadCertificates returns the certificates contained in pl.
+func (pl PEMLoader) LoadCertificates() ([]Certificate, error) {
+ var certs []Certificate
+ for i, pair := range pl {
+ cert, err := tls.X509KeyPair([]byte(pair.CertificatePEM), []byte(pair.KeyPEM))
+ if err != nil {
+ return nil, fmt.Errorf("PEM pair %d: %v", i, err)
+ }
+ certs = append(certs, Certificate{
+ Certificate: cert,
+ Tags: pair.Tags,
+ })
+ }
+ return certs, nil
+}
+
+// Interface guard
+var _ CertificateLoader = (PEMLoader)(nil)