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