| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | // 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" | 
					
						
							| 
									
										
										
										
											2022-03-25 11:28:54 -06:00
										 |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2022-03-25 11:28:54 -06:00
										 |  |  | 	"io/fs" | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	"log" | 
					
						
							| 
									
										
										
										
											2020-05-12 11:36:20 -06:00
										 |  |  | 	"runtime/debug" | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/caddyserver/caddy/v2" | 
					
						
							|  |  |  | 	"github.com/caddyserver/caddy/v2/modules/caddytls" | 
					
						
							| 
									
										
										
										
											2020-03-06 23:15:25 -07:00
										 |  |  | 	"github.com/caddyserver/certmagic" | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func init() { | 
					
						
							|  |  |  | 	caddy.RegisterModule(Provider{}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-10 13:36:46 -07:00
										 |  |  | // Provider implements a distributed STEK provider. This | 
					
						
							|  |  |  | // module will obtain STEKs from a storage module instead | 
					
						
							|  |  |  | // of generating STEKs internally. This allows STEKs to be | 
					
						
							|  |  |  | // coordinated, improving TLS session resumption in a cluster. | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | type Provider struct { | 
					
						
							| 
									
										
										
										
											2019-12-10 13:36:46 -07:00
										 |  |  | 	// The storage module wherein to store and obtain session | 
					
						
							|  |  |  | 	// ticket keys. If unset, Caddy's default/global-configured | 
					
						
							|  |  |  | 	// storage module will be used. | 
					
						
							|  |  |  | 	Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	storage    certmagic.Storage | 
					
						
							|  |  |  | 	stekConfig *caddytls.SessionTicketService | 
					
						
							|  |  |  | 	timer      *time.Timer | 
					
						
							| 
									
										
										
										
											2020-06-01 09:31:08 -06:00
										 |  |  | 	ctx        caddy.Context | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // CaddyModule returns the Caddy module information. | 
					
						
							|  |  |  | func (Provider) CaddyModule() caddy.ModuleInfo { | 
					
						
							|  |  |  | 	return caddy.ModuleInfo{ | 
					
						
							| 
									
										
										
										
											2019-12-10 13:36:46 -07:00
										 |  |  | 		ID:  "tls.stek.distributed", | 
					
						
							|  |  |  | 		New: func() caddy.Module { return new(Provider) }, | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Provision provisions s. | 
					
						
							|  |  |  | func (s *Provider) Provision(ctx caddy.Context) error { | 
					
						
							| 
									
										
										
										
											2020-06-01 09:31:08 -06:00
										 |  |  | 	s.ctx = ctx | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	// unpack the storage module to use, if different from the default | 
					
						
							|  |  |  | 	if s.Storage != nil { | 
					
						
							| 
									
										
										
										
											2019-12-10 13:36:46 -07:00
										 |  |  | 		val, err := ctx.LoadModule(s, "Storage") | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 		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 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// 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 | 
					
						
							| 
									
										
										
										
											2022-03-25 11:28:54 -06:00
										 |  |  | 	gobBytes, err := s.storage.Load(s.ctx, stekFileName) | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	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) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-03-25 11:28:54 -06:00
										 |  |  | 	err = s.storage.Store(s.ctx, stekFileName, buf.Bytes()) | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	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) { | 
					
						
							| 
									
										
										
										
											2020-11-22 16:50:29 -05:00
										 |  |  | 	err := s.storage.Lock(s.ctx, stekLockName) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return distributedSTEK{}, fmt.Errorf("failed to acquire storage lock: %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	//nolint:errcheck | 
					
						
							| 
									
										
										
										
											2022-03-25 11:28:54 -06:00
										 |  |  | 	defer s.storage.Unlock(s.ctx, stekLockName) | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// load the current STEKs from storage | 
					
						
							|  |  |  | 	dstek, err := s.loadSTEK() | 
					
						
							| 
									
										
										
										
											2022-03-25 11:28:54 -06:00
										 |  |  | 	if errors.Is(err, fs.ErrNotExist) { | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 		// 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) { | 
					
						
							| 
									
										
										
										
											2020-05-12 11:36:20 -06:00
										 |  |  | 	defer func() { | 
					
						
							|  |  |  | 		if err := recover(); err != nil { | 
					
						
							|  |  |  | 			log.Printf("[PANIC] distributed STEK rotation: %v\n%s", err, debug.Stack()) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}() | 
					
						
							| 
									
										
										
										
											2019-10-09 19:38:26 -06:00
										 |  |  | 	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) |