Implement session ticket keys; default STEK module with rotation

This commit is contained in:
Matthew Holt 2019-05-29 23:11:46 -06:00
parent 1b6b422c63
commit 3439933235
6 changed files with 453 additions and 99 deletions

View File

@ -13,6 +13,7 @@ import (
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/reverseproxy"
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/rewrite"
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls/standardstek"
)
func main() {

View File

@ -133,9 +133,21 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error {
},
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
// TODO: Session ticket key rotation (use Storage)
}
// session tickets support
cfg.SessionTicketsDisabled = tlsApp.SessionTickets.Disabled
// session ticket key rotation
tlsApp.SessionTickets.register(cfg)
ctx.OnCancel(func() {
// do cleanup when the context is cancelled because,
// though unlikely, it is possible that a context
// needing a TLS server config could exist for less
// than the lifetime of the whole app
tlsApp.SessionTickets.unregister(cfg)
})
// add all the cipher suites in order, without duplicates
cipherSuitesAdded := make(map[uint16]struct{})
for _, csName := range p.CipherSuites {

View File

@ -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
)

View File

@ -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)

View File

@ -2,14 +2,12 @@ package caddytls
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"bitbucket.org/lightcodelabs/caddy2"
"github.com/go-acme/lego/challenge"
"github.com/klauspost/cpuid"
"github.com/mholt/certmagic"
)
@ -22,8 +20,9 @@ func init() {
// TLS represents a process-wide TLS configuration.
type TLS struct {
Certificates map[string]json.RawMessage `json:"certificates"`
Automation AutomationConfig `json:"automation"`
Certificates map[string]json.RawMessage `json:"certificates,omitempty"`
Automation AutomationConfig `json:"automation,omitempty"`
SessionTickets SessionTicketService `json:"session_tickets,omitempty"`
certificateLoaders []CertificateLoader
certCache *certmagic.Cache
@ -44,6 +43,7 @@ func (t *TLS) Provision(ctx caddy2.Context) error {
},
})
// automation/management policies
for i, ap := range t.Automation.Policies {
val, err := ctx.LoadModuleInline("module", "tls.management", ap.Management)
if err != nil {
@ -65,6 +65,12 @@ func (t *TLS) Provision(ctx caddy2.Context) error {
t.certificateLoaders = append(t.certificateLoaders, val.(CertificateLoader))
}
// session ticket ephemeral keys (STEK) service and provider
err := t.SessionTickets.provision(ctx)
if err != nil {
return fmt.Errorf("provisioning session tickets configuration: %v", err)
}
return nil
}
@ -110,6 +116,7 @@ func (t *TLS) Stop() error {
// TODO: ensure locks are cleaned up too... maybe in certmagic though
t.certCache.Stop()
}
t.SessionTickets.stop()
return nil
}
@ -230,98 +237,4 @@ type ManagerMaker interface {
newManager(interactive bool) (certmagic.Manager, error)
}
// 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,
}
const automateKey = "automate"

102
modules/caddytls/values.go Normal file
View File

@ -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,
}