1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+// Package distributedstek provides TLS session ticket ephemeral
16
+// keys (STEKs) in a distributed fashion by utilizing configured
17
+// storage for locking and key sharing. This allows a cluster of
18
+// machines to optimally resume TLS sessions in a load-balanced
19
+// environment without any hassle. This is similar to what
20
+// Twitter does, but without needing to rely on SSH, as it is
21
+// built into the web server this way:
22
+// https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
23
+package distributedstek
24
+
25
+import (
26
+ "bytes"
27
+ "encoding/gob"
28
+ "encoding/json"
29
+ "fmt"
30
+ "log"
31
+ "time"
32
+
33
+ "github.com/caddyserver/caddy/v2"
34
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
35
+ "github.com/mholt/certmagic"
36
+)
37
+
38
+func init() {
39
+ caddy.RegisterModule(Provider{})
40
+}
41
+
42
+// Provider implements a distributed STEK provider.
43
+type Provider struct {
44
+ Storage json.RawMessage `json:"storage,omitempty"`
45
+
46
+ storage certmagic.Storage
47
+ stekConfig *caddytls.SessionTicketService
48
+ timer *time.Timer
49
+}
50
+
51
+// CaddyModule returns the Caddy module information.
52
+func (Provider) CaddyModule() caddy.ModuleInfo {
53
+ return caddy.ModuleInfo{
54
+ Name: "tls.stek.distributed",
55
+ New: func() caddy.Module { return new(Provider) },
56
+ }
57
+}
58
+
59
+// Provision provisions s.
60
+func (s *Provider) Provision(ctx caddy.Context) error {
61
+ // unpack the storage module to use, if different from the default
62
+ if s.Storage != nil {
63
+ val, err := ctx.LoadModuleInline("module", "caddy.storage", s.Storage)
64
+ if err != nil {
65
+ return fmt.Errorf("loading TLS storage module: %s", err)
66
+ }
67
+ cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
68
+ if err != nil {
69
+ return fmt.Errorf("creating TLS storage configuration: %v", err)
70
+ }
71
+ s.storage = cmStorage
72
+ s.Storage = nil // allow GC to deallocate
73
+ }
74
+
75
+ // otherwise, use default storage
76
+ if s.storage == nil {
77
+ s.storage = ctx.Storage()
78
+ }
79
+
80
+ return nil
81
+}
82
+
83
+// Initialize sets the configuration for s and returns the starting keys.
84
+func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
85
+ // keep a reference to the config; we'll need it when rotating keys
86
+ s.stekConfig = config
87
+
88
+ dstek, err := s.getSTEK()
89
+ if err != nil {
90
+ return nil, err
91
+ }
92
+
93
+ // create timer for the remaining time on the interval;
94
+ // this timer is cleaned up only when rotate() returns
95
+ s.timer = time.NewTimer(time.Until(dstek.NextRotation))
96
+
97
+ return dstek.Keys, nil
98
+}
99
+
100
+// Next returns a channel which transmits the latest session ticket keys.
101
+func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
102
+ keysChan := make(chan [][32]byte)
103
+ go s.rotate(doneChan, keysChan)
104
+ return keysChan
105
+}
106
+
107
+func (s *Provider) loadSTEK() (distributedSTEK, error) {
108
+ var sg distributedSTEK
109
+ gobBytes, err := s.storage.Load(stekFileName)
110
+ if err != nil {
111
+ return sg, err // don't wrap, in case error is certmagic.ErrNotExist
112
+ }
113
+ dec := gob.NewDecoder(bytes.NewReader(gobBytes))
114
+ err = dec.Decode(&sg)
115
+ if err != nil {
116
+ return sg, fmt.Errorf("STEK gob corrupted: %v", err)
117
+ }
118
+ return sg, nil
119
+}
120
+
121
+func (s *Provider) storeSTEK(dstek distributedSTEK) error {
122
+ var buf bytes.Buffer
123
+ err := gob.NewEncoder(&buf).Encode(dstek)
124
+ if err != nil {
125
+ return fmt.Errorf("encoding STEK gob: %v", err)
126
+ }
127
+ err = s.storage.Store(stekFileName, buf.Bytes())
128
+ if err != nil {
129
+ return fmt.Errorf("storing STEK gob: %v", err)
130
+ }
131
+ return nil
132
+}
133
+
134
+// getSTEK locks and loads the current STEK from storage. If none
135
+// currently exists, a new STEK is created and persisted. If the
136
+// current STEK is outdated (NextRotation time is in the past),
137
+// then it is rotated and persisted. The resulting STEK is returned.
138
+func (s *Provider) getSTEK() (distributedSTEK, error) {
139
+ s.storage.Lock(stekLockName)
140
+ defer s.storage.Unlock(stekLockName)
141
+
142
+ // load the current STEKs from storage
143
+ dstek, err := s.loadSTEK()
144
+ if _, isNotExist := err.(certmagic.ErrNotExist); isNotExist {
145
+ // if there is none, then make some right away
146
+ dstek, err = s.rotateKeys(dstek)
147
+ if err != nil {
148
+ return dstek, fmt.Errorf("creating new STEK: %v", err)
149
+ }
150
+ } else if err != nil {
151
+ // some other error, that's a problem
152
+ return dstek, fmt.Errorf("loading STEK: %v", err)
153
+ } else if time.Now().After(dstek.NextRotation) {
154
+ // if current STEKs are outdated, rotate them
155
+ dstek, err = s.rotateKeys(dstek)
156
+ if err != nil {
157
+ return dstek, fmt.Errorf("rotating keys: %v", err)
158
+ }
159
+ }
160
+
161
+ return dstek, nil
162
+}
163
+
164
+// rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
165
+// with updated keys and timestamps. It stores the returned STEK in storage,
166
+// so this function must only be called in a storage-provided lock.
167
+func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
168
+ var newSTEK distributedSTEK
169
+ var err error
170
+
171
+ newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
172
+ if err != nil {
173
+ return newSTEK, err
174
+ }
175
+
176
+ now := time.Now()
177
+ newSTEK.LastRotation = now
178
+ newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
179
+
180
+ err = s.storeSTEK(newSTEK)
181
+ if err != nil {
182
+ return newSTEK, err
183
+ }
184
+
185
+ return newSTEK, nil
186
+}
187
+
188
+// rotate rotates keys on a regular basis, sending each updated set of
189
+// keys down keysChan, until doneChan is closed.
190
+func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
191
+ for {
192
+ select {
193
+ case <-s.timer.C:
194
+ dstek, err := s.getSTEK()
195
+ if err != nil {
196
+ // TODO: improve this handling
197
+ log.Printf("[ERROR] Loading STEK: %v", err)
198
+ continue
199
+ }
200
+
201
+ // send the updated keys to the service
202
+ keysChan <- dstek.Keys
203
+
204
+ // timer channel is already drained, so reset directly (see godoc)
205
+ s.timer.Reset(time.Until(dstek.NextRotation))
206
+
207
+ case <-doneChan:
208
+ // again, see godocs for why timer is stopped this way
209
+ if !s.timer.Stop() {
210
+ <-s.timer.C
211
+ }
212
+ return
213
+ }
214
+ }
215
+}
216
+
217
+type distributedSTEK struct {
218
+ Keys [][32]byte
219
+ LastRotation, NextRotation time.Time
220
+}
221
+
222
+const (
223
+ stekLockName = "stek_check"
224
+ stekFileName = "stek/stek.bin"
225
+)
226
+
227
+// Interface guard
228
+var _ caddytls.STEKProvider = (*Provider)(nil)