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 caddytls
16
+
17
+import (
18
+ "context"
19
+ "io/fs"
20
+ "sync"
21
+ "testing"
22
+ "time"
23
+
24
+ "github.com/caddyserver/caddy/v2"
25
+ "github.com/caddyserver/certmagic"
26
+ "github.com/cloudflare/circl/hpke"
27
+ "go.uber.org/zap"
28
+)
29
+
30
+var (
31
+ testStorageMu sync.Mutex
32
+ activeTestStorage certmagic.Storage
33
+)
34
+
35
+type testStorageModule struct{}
36
+
37
+func (testStorageModule) CaddyModule() caddy.ModuleInfo {
38
+ return caddy.ModuleInfo{
39
+ ID: "caddy.storage.test_blocking",
40
+ New: func() caddy.Module { return new(testStorageModule) },
41
+ }
42
+}
43
+
44
+func (testStorageModule) CertMagicStorage() (certmagic.Storage, error) {
45
+ testStorageMu.Lock()
46
+ defer testStorageMu.Unlock()
47
+ return activeTestStorage, nil
48
+}
49
+
50
+func init() {
51
+ caddy.RegisterModule(testStorageModule{})
52
+}
53
+
54
+func newTestContextWithStorage(t *testing.T, storage certmagic.Storage) (caddy.Context, context.CancelFunc) {
55
+ testStorageMu.Lock()
56
+ activeTestStorage = storage
57
+ testStorageMu.Unlock()
58
+
59
+ cfg := &caddy.Config{
60
+ StorageRaw: []byte(`{"module": "test_blocking"}`),
61
+ }
62
+ ctx, err := caddy.ProvisionContext(cfg)
63
+ if err != nil {
64
+ t.Fatalf("caddy.ProvisionContext failed: %v", err)
65
+ }
66
+ return ctx, func() {
67
+ testStorageMu.Lock()
68
+ activeTestStorage = nil
69
+ testStorageMu.Unlock()
70
+ }
71
+}
72
+
73
+type blockingStorage struct {
74
+ lockFunc func(ctx context.Context, key string) error
75
+ unlockFunc func(ctx context.Context, key string) error
76
+ storeFunc func(ctx context.Context, key string, value []byte) error
77
+ loadFunc func(ctx context.Context, key string) ([]byte, error)
78
+ deleteFunc func(ctx context.Context, key string) error
79
+ existsFunc func(ctx context.Context, key string) bool
80
+ listFunc func(ctx context.Context, path string, recursive bool) ([]string, error)
81
+ statFunc func(ctx context.Context, key string) (certmagic.KeyInfo, error)
82
+}
83
+
84
+func (s *blockingStorage) Lock(ctx context.Context, key string) error {
85
+ if s.lockFunc != nil {
86
+ return s.lockFunc(ctx, key)
87
+ }
88
+ return nil
89
+}
90
+
91
+func (s *blockingStorage) Unlock(ctx context.Context, key string) error {
92
+ if s.unlockFunc != nil {
93
+ return s.unlockFunc(ctx, key)
94
+ }
95
+ return nil
96
+}
97
+
98
+func (s *blockingStorage) Store(ctx context.Context, key string, value []byte) error {
99
+ if s.storeFunc != nil {
100
+ return s.storeFunc(ctx, key, value)
101
+ }
102
+ return nil
103
+}
104
+
105
+func (s *blockingStorage) Load(ctx context.Context, key string) ([]byte, error) {
106
+ if s.loadFunc != nil {
107
+ return s.loadFunc(ctx, key)
108
+ }
109
+ return nil, fs.ErrNotExist
110
+}
111
+
112
+func (s *blockingStorage) Delete(ctx context.Context, key string) error {
113
+ if s.deleteFunc != nil {
114
+ return s.deleteFunc(ctx, key)
115
+ }
116
+ return nil
117
+}
118
+
119
+func (s *blockingStorage) Exists(ctx context.Context, key string) bool {
120
+ if s.existsFunc != nil {
121
+ return s.existsFunc(ctx, key)
122
+ }
123
+ return false
124
+}
125
+
126
+func (s *blockingStorage) List(ctx context.Context, path string, recursive bool) ([]string, error) {
127
+ if s.listFunc != nil {
128
+ return s.listFunc(ctx, path, recursive)
129
+ }
130
+ return nil, nil
131
+}
132
+
133
+func (s *blockingStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) {
134
+ if s.statFunc != nil {
135
+ return s.statFunc(ctx, key)
136
+ }
137
+ return certmagic.KeyInfo{}, fs.ErrNotExist
138
+}
139
+
140
+type blockingPublisher struct {
141
+ publishFunc func(ctx context.Context, innerNames []string, echConfigList []byte) error
142
+}
143
+
144
+func (p *blockingPublisher) PublisherKey() string {
145
+ return "blocking_test_publisher"
146
+}
147
+
148
+func (p *blockingPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, echConfigList []byte) error {
149
+ if p.publishFunc != nil {
150
+ return p.publishFunc(ctx, innerNames, echConfigList)
151
+ }
152
+ return nil
153
+}
154
+
155
+func TestTLSStorageCleanStopSynchronization(t *testing.T) {
156
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
157
+ defer cancel()
158
+
159
+ tlsApp := &TLS{
160
+ ctx: ctx,
161
+ logger: zap.NewNop(),
162
+ Automation: &AutomationConfig{
163
+ StorageCleanInterval: caddy.Duration(1 * time.Hour),
164
+ },
165
+ }
166
+
167
+ // Start storage cleaner
168
+ tlsApp.keepStorageClean()
169
+
170
+ // Stop must cancel cleaner context and wait for completion cleanly
171
+ err := tlsApp.Stop()
172
+ if err != nil {
173
+ t.Fatalf("TLS.Stop failed: %v", err)
174
+ }
175
+}
176
+
177
+func TestTLSStorageCleanUnitsCanceledContext(t *testing.T) {
178
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
179
+ cancel() // canceled immediately
180
+
181
+ tlsApp := &TLS{
182
+ ctx: ctx,
183
+ logger: zap.NewNop(),
184
+ }
185
+
186
+ // cleanStorageUnits with canceled context should return immediately
187
+ tlsApp.cleanStorageUnits(ctx.Context)
188
+}
189
+
190
+func TestTLSECHStopSynchronization(t *testing.T) {
191
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
192
+ defer cancel()
193
+
194
+ tlsApp := &TLS{
195
+ ctx: ctx,
196
+ logger: zap.NewNop(),
197
+ Automation: &AutomationConfig{},
198
+ EncryptedClientHello: &ECH{},
199
+ DisableStorageClean: true,
200
+ }
201
+
202
+ err := tlsApp.Start()
203
+ if err != nil {
204
+ t.Fatalf("TLS.Start failed: %v", err)
205
+ }
206
+
207
+ // Stop must cancel ECH context and wait for background ECH worker to exit
208
+ err = tlsApp.Stop()
209
+ if err != nil {
210
+ t.Fatalf("TLS.Stop failed: %v", err)
211
+ }
212
+}
213
+
214
+func TestTLSStorageCleanBlockingStorageStopSynchronization(t *testing.T) {
215
+ startedLock := make(chan struct{})
216
+ storage := &blockingStorage{
217
+ lockFunc: func(ctx context.Context, key string) error {
218
+ if key == "storage_clean" {
219
+ close(startedLock)
220
+ <-ctx.Done()
221
+ return ctx.Err()
222
+ }
223
+ return nil
224
+ },
225
+ }
226
+
227
+ ctx, cleanup := newTestContextWithStorage(t, storage)
228
+ defer cleanup()
229
+
230
+ tlsApp := &TLS{
231
+ ctx: ctx,
232
+ logger: zap.NewNop(),
233
+ Automation: &AutomationConfig{
234
+ StorageCleanInterval: caddy.Duration(1 * time.Hour),
235
+ },
236
+ }
237
+
238
+ // Reset storageClean timestamp so cleaning runs
239
+ storageCleanMu.Lock()
240
+ storageClean = time.Time{}
241
+ storageCleanMu.Unlock()
242
+
243
+ tlsApp.keepStorageClean()
244
+
245
+ select {
246
+ case <-startedLock:
247
+ case <-time.After(5 * time.Second):
248
+ t.Fatal("timed out waiting for storage lock to be attempted")
249
+ }
250
+
251
+ stopped := make(chan error, 1)
252
+ go func() {
253
+ stopped <- tlsApp.Stop()
254
+ }()
255
+
256
+ select {
257
+ case err := <-stopped:
258
+ if err != nil {
259
+ t.Fatalf("TLS.Stop failed: %v", err)
260
+ }
261
+ case <-time.After(5 * time.Second):
262
+ t.Fatal("TLS.Stop hung while storage cleaner was blocked on storage lock")
263
+ }
264
+}
265
+
266
+func TestTLSECHPublishBlockingStorageStopSynchronization(t *testing.T) {
267
+ startedLock := make(chan struct{})
268
+ storage := &blockingStorage{
269
+ lockFunc: func(ctx context.Context, key string) error {
270
+ if key == "ech_publish" {
271
+ close(startedLock)
272
+ <-ctx.Done()
273
+ return ctx.Err()
274
+ }
275
+ return nil
276
+ },
277
+ }
278
+
279
+ ctx, cleanup := newTestContextWithStorage(t, storage)
280
+ defer cleanup()
281
+
282
+ ech := &ECH{
283
+ configsMu: new(sync.RWMutex),
284
+ Configs: []ECHConfiguration{{PublicName: "example.com"}},
285
+ }
286
+ tlsApp := &TLS{
287
+ ctx: ctx,
288
+ logger: zap.NewNop(),
289
+ Automation: &AutomationConfig{},
290
+ EncryptedClientHello: ech,
291
+ DisableStorageClean: true,
292
+ }
293
+
294
+ err := tlsApp.Start()
295
+ if err != nil {
296
+ t.Fatalf("TLS.Start failed: %v", err)
297
+ }
298
+
299
+ select {
300
+ case <-startedLock:
301
+ case <-time.After(5 * time.Second):
302
+ t.Fatal("timed out waiting for ech_publish storage lock to be attempted")
303
+ }
304
+
305
+ stopped := make(chan error, 1)
306
+ go func() {
307
+ stopped <- tlsApp.Stop()
308
+ }()
309
+
310
+ select {
311
+ case err := <-stopped:
312
+ if err != nil {
313
+ t.Fatalf("TLS.Stop failed: %v", err)
314
+ }
315
+ case <-time.After(5 * time.Second):
316
+ t.Fatal("TLS.Stop hung while ECH publisher was blocked on ech_publish storage lock")
317
+ }
318
+}
319
+
320
+func TestTLSECHPublishBlockingPublisherStopSynchronization(t *testing.T) {
321
+ startedPublish := make(chan struct{})
322
+ publisher := &blockingPublisher{
323
+ publishFunc: func(ctx context.Context, innerNames []string, echConfigList []byte) error {
324
+ close(startedPublish)
325
+ <-ctx.Done()
326
+ return ctx.Err()
327
+ },
328
+ }
329
+
330
+ storage := &blockingStorage{}
331
+ ctx, cleanup := newTestContextWithStorage(t, storage)
332
+ defer cleanup()
333
+
334
+ publicKey, _, err := hpke.KEM_X25519_HKDF_SHA256.Scheme().GenerateKeyPair()
335
+ if err != nil {
336
+ t.Fatalf("GenerateKeyPair failed: %v", err)
337
+ }
338
+
339
+ ech := &ECH{
340
+ configsMu: new(sync.RWMutex),
341
+ Configs: []ECHConfiguration{{PublicName: "example.com"}},
342
+ Publication: []*ECHPublication{
343
+ {
344
+ Domains: []string{"example.com"},
345
+ publishers: []ECHPublisher{publisher},
346
+ },
347
+ },
348
+ configs: map[string][]echConfig{
349
+ "example.com": {
350
+ {
351
+ PublicKey: publicKey,
352
+ Version: draftTLSESNI25,
353
+ ConfigID: 1,
354
+ RawPublicName: "example.com",
355
+ KEMID: hpke.KEM_X25519_HKDF_SHA256,
356
+ CipherSuites: []hpkeSymmetricCipherSuite{
357
+ {
358
+ KDFID: hpke.KDF_HKDF_SHA256,
359
+ AEADID: hpke.AEAD_AES128GCM,
360
+ },
361
+ },
362
+ meta: echConfigMeta{
363
+ Publications: make(publicationHistory),
364
+ },
365
+ },
366
+ },
367
+ },
368
+ }
369
+ tlsApp := &TLS{
370
+ ctx: ctx,
371
+ logger: zap.NewNop(),
372
+ Automation: &AutomationConfig{},
373
+ EncryptedClientHello: ech,
374
+ DisableStorageClean: true,
375
+ serverNames: map[string]serverNameRegistration{
376
+ "example.com": {},
377
+ },
378
+ }
379
+
380
+ err = tlsApp.Start()
381
+ if err != nil {
382
+ t.Fatalf("TLS.Start failed: %v", err)
383
+ }
384
+
385
+ select {
386
+ case <-startedPublish:
387
+ case <-time.After(5 * time.Second):
388
+ t.Fatal("timed out waiting for ECH publication to be attempted")
389
+ }
390
+
391
+ stopped := make(chan error, 1)
392
+ go func() {
393
+ stopped <- tlsApp.Stop()
394
+ }()
395
+
396
+ select {
397
+ case err := <-stopped:
398
+ if err != nil {
399
+ t.Fatalf("TLS.Stop failed: %v", err)
400
+ }
401
+ case <-time.After(5 * time.Second):
402
+ t.Fatal("TLS.Stop hung while ECH publisher was blocked inside PublishECHConfigList")
403
+ }
404
+}
405
+
406
+func TestTLSECHRotateBlockingStorageStopSynchronization(t *testing.T) {
407
+ startedLock := make(chan struct{})
408
+ storage := &blockingStorage{
409
+ lockFunc: func(ctx context.Context, key string) error {
410
+ if key == echStorageLockName {
411
+ close(startedLock)
412
+ <-ctx.Done()
413
+ return ctx.Err()
414
+ }
415
+ return nil
416
+ },
417
+ }
418
+
419
+ ctx, cleanup := newTestContextWithStorage(t, storage)
420
+ defer cleanup()
421
+
422
+ ech := &ECH{
423
+ configsMu: new(sync.RWMutex),
424
+ Configs: []ECHConfiguration{{PublicName: "example.com"}},
425
+ configs: map[string][]echConfig{
426
+ "example.com": {
427
+ {
428
+ ConfigID: 1,
429
+ RawPublicName: "example.com",
430
+ meta: echConfigMeta{
431
+ Created: time.Now().Add(-24 * time.Hour * 31), // > 30 days old so rotationNeeded returns true
432
+ Publications: make(publicationHistory),
433
+ },
434
+ },
435
+ },
436
+ },
437
+ }
438
+ tlsApp := &TLS{
439
+ ctx: ctx,
440
+ logger: zap.NewNop(),
441
+ Automation: &AutomationConfig{},
442
+ EncryptedClientHello: ech,
443
+ DisableStorageClean: true,
444
+ echRotateInterval: 5 * time.Millisecond,
445
+ }
446
+
447
+ err := tlsApp.Start()
448
+ if err != nil {
449
+ t.Fatalf("TLS.Start failed: %v", err)
450
+ }
451
+
452
+ select {
453
+ case <-startedLock:
454
+ case <-time.After(5 * time.Second):
455
+ t.Fatal("timed out waiting for echStorageLockName storage lock to be attempted during rotation")
456
+ }
457
+
458
+ stopped := make(chan error, 1)
459
+ go func() {
460
+ stopped <- tlsApp.Stop()
461
+ }()
462
+
463
+ select {
464
+ case err := <-stopped:
465
+ if err != nil {
466
+ t.Fatalf("TLS.Stop failed: %v", err)
467
+ }
468
+ case <-time.After(5 * time.Second):
469
+ t.Fatal("TLS.Stop hung while ECH key rotation was blocked on storage lock (context was likely ignored)")
470
+ }
471
+}