mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-22 10:25:46 +03:00
server: Rotate TLS ticket "keys" (#742)
This commit is contained in:
parent
ac80f6edc3
commit
b149a86bc2
2 changed files with 139 additions and 0 deletions
|
@ -4,9 +4,11 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -18,6 +20,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tlsNewTicketEvery = time.Hour * 10 // generate a new ticket for TLS PFS encryption every so often
|
||||||
|
tlsNumTickets = 4 // hold and consider that many tickets to decrypt TLS sessions
|
||||||
|
)
|
||||||
|
|
||||||
// Server represents an instance of a server, which serves
|
// Server represents an instance of a server, which serves
|
||||||
// HTTP requests at a particular address (host and port). A
|
// HTTP requests at a particular address (host and port). A
|
||||||
// server is capable of serving numerous virtual hosts on
|
// server is capable of serving numerous virtual hosts on
|
||||||
|
@ -28,6 +35,7 @@ type Server struct {
|
||||||
HTTP2 bool // whether to enable HTTP/2
|
HTTP2 bool // whether to enable HTTP/2
|
||||||
tls bool // whether this server is serving all HTTPS hosts or not
|
tls bool // whether this server is serving all HTTPS hosts or not
|
||||||
OnDemandTLS bool // whether this server supports on-demand TLS (load certs at handshake-time)
|
OnDemandTLS bool // whether this server supports on-demand TLS (load certs at handshake-time)
|
||||||
|
tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine
|
||||||
vhosts map[string]virtualHost // virtual hosts keyed by their address
|
vhosts map[string]virtualHost // virtual hosts keyed by their address
|
||||||
listener ListenerFile // the listener which is bound to the socket
|
listener ListenerFile // the listener which is bound to the socket
|
||||||
listenerMu sync.Mutex // protects listener
|
listenerMu sync.Mutex // protects listener
|
||||||
|
@ -216,6 +224,11 @@ func serveTLS(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup any goroutines governing over TLS settings
|
||||||
|
s.tlsGovChan = make(chan struct{})
|
||||||
|
timer := time.NewTicker(tlsNewTicketEvery)
|
||||||
|
go runTLSTicketKeyRotation(s.TLSConfig, timer, s.tlsGovChan)
|
||||||
|
|
||||||
// Create TLS listener - note that we do not replace s.listener
|
// Create TLS listener - note that we do not replace s.listener
|
||||||
// with this TLS listener; tls.listener is unexported and does
|
// with this TLS listener; tls.listener is unexported and does
|
||||||
// not implement the File() method we need for graceful restarts
|
// not implement the File() method we need for graceful restarts
|
||||||
|
@ -258,6 +271,11 @@ func (s *Server) Stop() (err error) {
|
||||||
}
|
}
|
||||||
s.listenerMu.Unlock()
|
s.listenerMu.Unlock()
|
||||||
|
|
||||||
|
// Closing this signals any TLS governor goroutines to exit
|
||||||
|
if s.tlsGovChan != nil {
|
||||||
|
close(s.tlsGovChan)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,6 +396,67 @@ func setupClientAuth(tlsConfigs []TLSConfig, config *tls.Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var runTLSTicketKeyRotation = standaloneTLSTicketKeyRotation
|
||||||
|
|
||||||
|
var setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte {
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// standaloneTLSTicketKeyRotation governs over the array of TLS ticket keys used to de/crypt TLS tickets.
|
||||||
|
// It periodically sets a new ticket key as the first one, used to encrypt (and decrypt),
|
||||||
|
// pushing any old ticket keys to the back, where they are considered for decryption only.
|
||||||
|
//
|
||||||
|
// Lack of entropy for the very first ticket key results in the feature being disabled (as does Go),
|
||||||
|
// later lack of entropy temporarily disables ticket key rotation.
|
||||||
|
// Old ticket keys are still phased out, though.
|
||||||
|
//
|
||||||
|
// Stops the timer when returning.
|
||||||
|
func standaloneTLSTicketKeyRotation(c *tls.Config, timer *time.Ticker, exitChan chan struct{}) {
|
||||||
|
defer timer.Stop()
|
||||||
|
// The entire page should be marked as sticky, but Go cannot do that
|
||||||
|
// without resorting to syscall#Mlock. And, we don't have madvise (for NODUMP), too. ☹
|
||||||
|
keys := make([][32]byte, 1, tlsNumTickets)
|
||||||
|
|
||||||
|
rng := c.Rand
|
||||||
|
if rng == nil {
|
||||||
|
rng = rand.Reader
|
||||||
|
}
|
||||||
|
if _, err := io.ReadFull(rng, keys[0][:]); err != nil {
|
||||||
|
c.SessionTicketsDisabled = true // bail if we don't have the entropy for the first one
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys))
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, isOpen := <-exitChan:
|
||||||
|
if !isOpen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-timer.C:
|
||||||
|
rng = c.Rand // could've changed since the start
|
||||||
|
if rng == nil {
|
||||||
|
rng = rand.Reader
|
||||||
|
}
|
||||||
|
var newTicketKey [32]byte
|
||||||
|
_, err := io.ReadFull(rng, newTicketKey[:])
|
||||||
|
|
||||||
|
if len(keys) < tlsNumTickets {
|
||||||
|
keys = append(keys, keys[0]) // manipulates the internal length
|
||||||
|
}
|
||||||
|
for idx := len(keys) - 1; idx >= 1; idx-- {
|
||||||
|
keys[idx] = keys[idx-1] // yes, this makes copies
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
keys[0] = newTicketKey
|
||||||
|
}
|
||||||
|
// pushes the last key out, doesn't matter that we don't have a new one
|
||||||
|
c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RunFirstStartupFuncs runs all of the server's FirstStartup
|
// RunFirstStartupFuncs runs all of the server's FirstStartup
|
||||||
// callback functions unless one of them returns an error first.
|
// callback functions unless one of them returns an error first.
|
||||||
// It is the caller's responsibility to call this only once and
|
// It is the caller's responsibility to call this only once and
|
||||||
|
|
60
server/server_test.go
Normal file
60
server/server_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStandaloneTLSTicketKeyRotation(t *testing.T) {
|
||||||
|
tlsGovChan := make(chan struct{})
|
||||||
|
defer close(tlsGovChan)
|
||||||
|
callSync := make(chan bool, 1)
|
||||||
|
defer close(callSync)
|
||||||
|
|
||||||
|
oldHook := setSessionTicketKeysTestHook
|
||||||
|
defer func() {
|
||||||
|
setSessionTicketKeysTestHook = oldHook
|
||||||
|
}()
|
||||||
|
var keysInUse [][32]byte
|
||||||
|
setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte {
|
||||||
|
keysInUse = keys
|
||||||
|
callSync <- true
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(tls.Config)
|
||||||
|
timer := time.NewTicker(time.Millisecond * 1)
|
||||||
|
|
||||||
|
go standaloneTLSTicketKeyRotation(c, timer, tlsGovChan)
|
||||||
|
|
||||||
|
rounds := 0
|
||||||
|
var lastTicketKey [32]byte
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-callSync:
|
||||||
|
if lastTicketKey == keysInUse[0] {
|
||||||
|
close(tlsGovChan)
|
||||||
|
t.Errorf("The same TLS ticket key has been used again (not rotated): %x.", lastTicketKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastTicketKey = keysInUse[0]
|
||||||
|
rounds++
|
||||||
|
if rounds <= tlsNumTickets && len(keysInUse) != rounds {
|
||||||
|
close(tlsGovChan)
|
||||||
|
t.Errorf("Expected TLS ticket keys in use: %d; Got instead: %d.", rounds, len(keysInUse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.SessionTicketsDisabled == true {
|
||||||
|
t.Error("Session tickets have been disabled unexpectedly.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rounds >= tlsNumTickets+1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 1):
|
||||||
|
t.Errorf("Timeout after %d rounds.", rounds)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue