Matthew Holt
6 years ago
6 changed files with 453 additions and 99 deletions
@ -0,0 +1,214 @@ |
|||
package caddytls |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"crypto/tls" |
|||
"encoding/json" |
|||
"fmt" |
|||
"io" |
|||
"sync" |
|||
"time" |
|||
|
|||
"bitbucket.org/lightcodelabs/caddy2" |
|||
) |
|||
|
|||
// SessionTicketService configures and manages TLS session tickets.
|
|||
type SessionTicketService struct { |
|||
KeySource json.RawMessage `json:"key_source,omitempty"` |
|||
RotationInterval caddy2.Duration `json:"rotation_interval,omitempty"` |
|||
MaxKeys int `json:"max_keys,omitempty"` |
|||
DisableRotation bool `json:"disable_rotation,omitempty"` |
|||
Disabled bool `json:"disabled,omitempty"` |
|||
|
|||
keySource STEKProvider |
|||
configs map[*tls.Config]struct{} |
|||
stopChan chan struct{} |
|||
currentKeys [][32]byte |
|||
mu *sync.Mutex |
|||
} |
|||
|
|||
func (s *SessionTicketService) provision(ctx caddy2.Context) error { |
|||
s.configs = make(map[*tls.Config]struct{}) |
|||
s.mu = new(sync.Mutex) |
|||
|
|||
// establish sane defaults
|
|||
if s.RotationInterval == 0 { |
|||
s.RotationInterval = caddy2.Duration(defaultSTEKRotationInterval) |
|||
} |
|||
if s.MaxKeys <= 0 { |
|||
s.MaxKeys = defaultMaxSTEKs |
|||
} |
|||
if s.KeySource == nil { |
|||
s.KeySource = json.RawMessage(`{"provider":"standard"}`) |
|||
} |
|||
|
|||
// load the STEK module, which will provide keys
|
|||
val, err := ctx.LoadModuleInline("provider", "tls.stek", s.KeySource) |
|||
if err != nil { |
|||
return fmt.Errorf("loading TLS session ticket ephemeral keys provider module: %s", err) |
|||
} |
|||
s.keySource = val.(STEKProvider) |
|||
s.KeySource = nil // allow GC to deallocate - TODO: Does this help?
|
|||
|
|||
// if session tickets or just rotation are
|
|||
// disabled, no need to start service
|
|||
if s.Disabled || s.DisableRotation { |
|||
return nil |
|||
} |
|||
|
|||
// start the STEK module; this ensures we have
|
|||
// a starting key before any config needs one
|
|||
return s.start() |
|||
} |
|||
|
|||
// start loads the starting STEKs and spawns a goroutine
|
|||
// which loops to rotate the STEKs, which continues until
|
|||
// stop() is called. If start() was already called, this
|
|||
// is a no-op.
|
|||
func (s *SessionTicketService) start() error { |
|||
if s.stopChan != nil { |
|||
return nil |
|||
} |
|||
s.stopChan = make(chan struct{}) |
|||
|
|||
// initializing the key source gives us our
|
|||
// initial key(s) to start with; if successful,
|
|||
// we need to be sure to call Next() so that
|
|||
// the key source can know when it is done
|
|||
initialKeys, err := s.keySource.Initialize(s) |
|||
if err != nil { |
|||
return fmt.Errorf("setting STEK module configuration: %v", err) |
|||
} |
|||
|
|||
s.mu.Lock() |
|||
s.currentKeys = initialKeys |
|||
s.mu.Unlock() |
|||
|
|||
// keep the keys rotated
|
|||
go s.stayUpdated() |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// stayUpdated is a blocking function which rotates
|
|||
// the keys whenever new ones are sent. It reads
|
|||
// from keysChan until s.stop() is called.
|
|||
func (s *SessionTicketService) stayUpdated() { |
|||
// this call is essential when Initialize()
|
|||
// returns without error, because the stop
|
|||
// channel is the only way the key source
|
|||
// will know when to clean up
|
|||
keysChan := s.keySource.Next(s.stopChan) |
|||
|
|||
for { |
|||
select { |
|||
case newKeys := <-keysChan: |
|||
s.mu.Lock() |
|||
s.currentKeys = newKeys |
|||
configs := s.configs |
|||
s.mu.Unlock() |
|||
for cfg := range configs { |
|||
cfg.SetSessionTicketKeys(newKeys) |
|||
} |
|||
case <-s.stopChan: |
|||
return |
|||
} |
|||
} |
|||
} |
|||
|
|||
// stop terminates the key rotation goroutine.
|
|||
func (s *SessionTicketService) stop() { |
|||
if s.stopChan != nil { |
|||
close(s.stopChan) |
|||
} |
|||
} |
|||
|
|||
// register sets the session ticket keys on cfg
|
|||
// and keeps them updated. Any values registered
|
|||
// must be unregistered, or they will not be
|
|||
// garbage-collected. s.start() must have been
|
|||
// called first. If session tickets are disabled
|
|||
// or if ticket key rotation is disabled, this
|
|||
// function is a no-op.
|
|||
func (s *SessionTicketService) register(cfg *tls.Config) { |
|||
if s.Disabled || s.DisableRotation { |
|||
return |
|||
} |
|||
s.mu.Lock() |
|||
cfg.SetSessionTicketKeys(s.currentKeys) |
|||
s.configs[cfg] = struct{}{} |
|||
s.mu.Unlock() |
|||
} |
|||
|
|||
// unregister stops session key management on cfg and
|
|||
// removes the internal stored reference to cfg. If
|
|||
// session tickets are disabled or if ticket key rotation
|
|||
// is disabled, this function is a no-op.
|
|||
func (s *SessionTicketService) unregister(cfg *tls.Config) { |
|||
if s.Disabled || s.DisableRotation { |
|||
return |
|||
} |
|||
s.mu.Lock() |
|||
delete(s.configs, cfg) |
|||
s.mu.Unlock() |
|||
} |
|||
|
|||
// RotateSTEKs rotates the keys in keys by producing a new key and eliding
|
|||
// the oldest one. The new slice of keys is returned.
|
|||
func (s SessionTicketService) RotateSTEKs(keys [][32]byte) ([][32]byte, error) { |
|||
// produce a new key
|
|||
newKey, err := s.generateSTEK() |
|||
if err != nil { |
|||
return nil, fmt.Errorf("generating STEK: %v", err) |
|||
} |
|||
|
|||
// we need to prepend this new key to the list of
|
|||
// keys so that it is preferred, but we need to be
|
|||
// careful that we do not grow the slice larger
|
|||
// than MaxKeys, otherwise we'll be storing one
|
|||
// more key in memory than we expect; so be sure
|
|||
// that the slice does not grow beyond the limit
|
|||
// even for a brief period of time, since there's
|
|||
// no guarantee when that extra allocation will
|
|||
// be overwritten; this is why we first trim the
|
|||
// length to one less the max, THEN prepend the
|
|||
// new key
|
|||
if len(keys) >= s.MaxKeys { |
|||
keys[len(keys)-1] = [32]byte{} // zero-out memory of oldest key
|
|||
keys = keys[:s.MaxKeys-1] // trim length of slice
|
|||
} |
|||
keys = append([][32]byte{newKey}, keys...) // prepend new key
|
|||
|
|||
return keys, nil |
|||
} |
|||
|
|||
// generateSTEK generates key material suitable for use as a
|
|||
// session ticket ephemeral key.
|
|||
func (s *SessionTicketService) generateSTEK() ([32]byte, error) { |
|||
var newTicketKey [32]byte |
|||
_, err := io.ReadFull(rand.Reader, newTicketKey[:]) |
|||
return newTicketKey, err |
|||
} |
|||
|
|||
// STEKProvider is a type that can provide session ticket ephemeral
|
|||
// keys (STEKs).
|
|||
type STEKProvider interface { |
|||
// Initialize provides the STEK configuration to the STEK
|
|||
// module so that it can obtain and manage keys accordingly.
|
|||
// It returns the initial key(s) to use. Implementations can
|
|||
// rely on Next() being called if Initialize() returns
|
|||
// without error, so that it may know when it is done.
|
|||
Initialize(config *SessionTicketService) ([][32]byte, error) |
|||
|
|||
// Next returns the channel through which the next session
|
|||
// ticket keys will be transmitted until doneChan is closed.
|
|||
// Keys should be sent on keysChan as they are updated.
|
|||
// When doneChan is closed, any resources allocated in
|
|||
// Initialize() must be cleaned up.
|
|||
Next(doneChan <-chan struct{}) (keysChan <-chan [][32]byte) |
|||
} |
|||
|
|||
const ( |
|||
defaultSTEKRotationInterval = 12 * time.Hour |
|||
defaultMaxSTEKs = 4 |
|||
) |
@ -0,0 +1,112 @@ |
|||
package standardstek |
|||
|
|||
import ( |
|||
"log" |
|||
"sync" |
|||
"time" |
|||
|
|||
"bitbucket.org/lightcodelabs/caddy2" |
|||
"bitbucket.org/lightcodelabs/caddy2/modules/caddytls" |
|||
) |
|||
|
|||
func init() { |
|||
caddy2.RegisterModule(caddy2.Module{ |
|||
Name: "tls.stek.standard", |
|||
New: func() interface{} { return new(standardSTEKProvider) }, |
|||
}) |
|||
} |
|||
|
|||
type standardSTEKProvider struct { |
|||
stekConfig *caddytls.SessionTicketService |
|||
timer *time.Timer |
|||
} |
|||
|
|||
// Initialize sets the configuration for s and returns the starting keys.
|
|||
func (s *standardSTEKProvider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) { |
|||
// keep a reference to the config, we'll need when rotating keys
|
|||
s.stekConfig = config |
|||
|
|||
itvl := time.Duration(s.stekConfig.RotationInterval) |
|||
|
|||
mutex.Lock() |
|||
defer mutex.Unlock() |
|||
|
|||
// if this is our first rotation or we are overdue
|
|||
// for one, perform a rotation immediately; otherwise,
|
|||
// we assume that the keys are non-empty and fresh
|
|||
since := time.Since(lastRotation) |
|||
if lastRotation.IsZero() || since > itvl { |
|||
var err error |
|||
keys, err = s.stekConfig.RotateSTEKs(keys) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
since = 0 // since this is overdue or is the first rotation, use full interval
|
|||
lastRotation = time.Now() |
|||
} |
|||
|
|||
// create timer for the remaining time on the interval;
|
|||
// this timer is cleaned up only when Next() returns
|
|||
s.timer = time.NewTimer(itvl - since) |
|||
|
|||
return keys, nil |
|||
} |
|||
|
|||
// Next returns a channel which transmits the latest session ticket keys.
|
|||
func (s *standardSTEKProvider) Next(doneChan <-chan struct{}) <-chan [][32]byte { |
|||
keysChan := make(chan [][32]byte) |
|||
go s.rotate(doneChan, keysChan) |
|||
return keysChan |
|||
} |
|||
|
|||
// rotate rotates keys on a regular basis, sending each updated set of
|
|||
// keys down keysChan, until doneChan is closed.
|
|||
func (s *standardSTEKProvider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) { |
|||
for { |
|||
select { |
|||
case now := <-s.timer.C: |
|||
// copy the slice header to avoid races
|
|||
mutex.RLock() |
|||
keysCopy := keys |
|||
mutex.RUnlock() |
|||
|
|||
// generate a new key, rotating old ones
|
|||
var err error |
|||
keysCopy, err = s.stekConfig.RotateSTEKs(keysCopy) |
|||
if err != nil { |
|||
// TODO: improve this handling
|
|||
log.Printf("[ERROR] Generating STEK: %v", err) |
|||
continue |
|||
} |
|||
|
|||
// replace keys slice with updated value and
|
|||
// record the timestamp of rotation
|
|||
mutex.Lock() |
|||
keys = keysCopy |
|||
lastRotation = now |
|||
mutex.Unlock() |
|||
|
|||
// send the updated keys to the service
|
|||
keysChan <- keysCopy |
|||
|
|||
// timer channel is already drained, so reset directly (see godoc)
|
|||
s.timer.Reset(time.Duration(s.stekConfig.RotationInterval)) |
|||
|
|||
case <-doneChan: |
|||
// again, see godocs for why timer is stopped this way
|
|||
if !s.timer.Stop() { |
|||
<-s.timer.C |
|||
} |
|||
return |
|||
} |
|||
} |
|||
} |
|||
|
|||
var ( |
|||
lastRotation time.Time |
|||
keys [][32]byte |
|||
mutex sync.RWMutex // protects keys and lastRotation
|
|||
) |
|||
|
|||
// Interface guard
|
|||
var _ caddytls.STEKProvider = (*standardSTEKProvider)(nil) |
@ -0,0 +1,102 @@ |
|||
package caddytls |
|||
|
|||
import ( |
|||
"crypto/tls" |
|||
"crypto/x509" |
|||
|
|||
"github.com/klauspost/cpuid" |
|||
) |
|||
|
|||
// 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, |
|||
} |
|||
|
|||
// publicKeyAlgorithms is the map of supported public key algorithms.
|
|||
var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{ |
|||
"rsa": x509.RSA, |
|||
"dsa": x509.DSA, |
|||
"ecdsa": x509.ECDSA, |
|||
} |
Loading…
Reference in new issue