Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 10, 23:45 UTC
caddytls: sync ech publication history to the ech configs (#8376)
* sync ech publication history to the ech configs * sync ech publication history for subsequent publishers * Add ECH multi-publisher regression test Co-authored-by: WeidiDeng <28780594+WeidiDeng@users.noreply.github.com> * create publication history maps before it's copied * update comments * fix lint --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: WeidiDeng <28780594+WeidiDeng@users.noreply.github.com>
WeidiDeng committed
Nov 8, 2026 at 19:18 UTC
daa74db1d1946e3c0343dfd9b93fd86836afb6ab
2 files changed
+119
-10
modules/caddytls/ech.go
+16
-5
@@ -329,6 +329,18 @@ func (ech *ECH) updateKeyList() {
329
}
330
}
331
332
+// addPublicationHistoryToEchConfigList is needed to initialize the publication history
333
+// because without it, caddy won't remember successfully published ech records in memory.
334
+// Ranging over the config slice copies each struct, so initializing a map on
335
+// a copy would not update the original config. Initialize it before copying instead.
336
+func addPublicationHistoryToEchConfigList(echCfgList echConfigList) {
337
+ for i := range echCfgList {
338
+ if echCfgList[i].meta.Publications == nil {
339
+ echCfgList[i].meta.Publications = make(publicationHistory)
340
+ }
341
+ }
342
+}
343
+
344
// publishECHConfigs publishes any configs that are configured for publication and which haven't been published already.
345
func (t *TLS) publishECHConfigs(ctx context.Context, logger *zap.Logger) error {
346
if err := ctx.Err(); err != nil {
@@ -378,7 +390,7 @@ func (t *TLS) publishECHConfigs(ctx context.Context, logger *zap.Logger) error {
390
// publish with it, and figure out which inner names to publish
391
// to/for, then publish
392
for _, publication := range publicationList {
381
- t.EncryptedClientHello.configsMu.RLock()
393
+ t.EncryptedClientHello.configsMu.Lock()
394
// this publication is either configured for specific ECH configs,
395
// or we just use an implied default of all ECH configs
396
var echCfgList echConfigList
@@ -386,6 +398,7 @@ func (t *TLS) publishECHConfigs(ctx context.Context, logger *zap.Logger) error {
398
if publication.Configs == nil {
399
// by default, publish all configs
400
for _, configs := range t.EncryptedClientHello.configs {
401
+ addPublicationHistoryToEchConfigList(configs)
402
echCfgList = append(echCfgList, configs...)
403
for _, c := range configs {
404
configIDs = append(configIDs, c.ConfigID)
@@ -394,6 +407,7 @@ func (t *TLS) publishECHConfigs(ctx context.Context, logger *zap.Logger) error {
407
} else {
408
for _, cfgOuterName := range publication.Configs {
409
if cfgList, ok := t.EncryptedClientHello.configs[cfgOuterName]; ok {
410
+ addPublicationHistoryToEchConfigList(cfgList)
411
echCfgList = append(echCfgList, cfgList...)
412
for _, c := range cfgList {
413
configIDs = append(configIDs, c.ConfigID)
@@ -401,7 +415,7 @@ func (t *TLS) publishECHConfigs(ctx context.Context, logger *zap.Logger) error {
415
}
416
}
417
}
404
- t.EncryptedClientHello.configsMu.RUnlock()
418
+ t.EncryptedClientHello.configsMu.Unlock()
419
420
// marshal the ECH config list as binary for publication
421
echCfgListBin, err := echCfgList.MarshalBinary()
@@ -519,9 +533,6 @@ func (t *TLS) publishECHConfigs(ctx context.Context, logger *zap.Logger) error {
533
zap.Uint8s("config_ids", configIDs))
534
535
for _, cfg := range echCfgList {
522
- if cfg.meta.Publications == nil {
523
- cfg.meta.Publications = make(publicationHistory)
524
- }
536
if _, ok := cfg.meta.Publications[publisherKey]; !ok {
537
cfg.meta.Publications[publisherKey] = make(map[string]time.Time)
538
}
modules/caddytls/storage_clean_test.go
+103
-5
@@ -16,6 +16,7 @@ package caddytls
16
17
import (
18
"context"
19
+ "encoding/json"
20
"io/fs"
21
"sync"
22
"testing"
@@ -138,10 +139,14 @@ func (s *blockingStorage) Stat(ctx context.Context, key string) (certmagic.KeyIn
139
}
140
141
type blockingPublisher struct {
142
+ key string
143
publishFunc func(ctx context.Context, innerNames []string, echConfigList []byte) error
144
}
145
146
func (p *blockingPublisher) PublisherKey() string {
147
+ if p.key != "" {
148
+ return p.key
149
+ }
150
return "blocking_test_publisher"
151
}
152
@@ -152,6 +157,103 @@ func (p *blockingPublisher) PublishECHConfigList(ctx context.Context, innerNames
157
return nil
158
}
159
160
+func TestTLSECHPublishTracksMultiplePublishers(t *testing.T) {
161
+ storedMetadata := make(map[string][]byte)
162
+ storage := &blockingStorage{
163
+ storeFunc: func(_ context.Context, key string, value []byte) error {
164
+ storedMetadata[key] = append([]byte(nil), value...)
165
+ return nil
166
+ },
167
+ }
168
+ ctx, cleanup := newTestContextWithStorage(t, storage)
169
+ defer cleanup()
170
+
171
+ publishCalls := make(map[string]int)
172
+ publisher1 := &blockingPublisher{
173
+ key: "publisher1",
174
+ publishFunc: func(context.Context, []string, []byte) error {
175
+ publishCalls["publisher1"]++
176
+ return nil
177
+ },
178
+ }
179
+ publisher2 := &blockingPublisher{
180
+ key: "publisher2",
181
+ publishFunc: func(context.Context, []string, []byte) error {
182
+ publishCalls["publisher2"]++
183
+ return nil
184
+ },
185
+ }
186
+
187
+ publicKey, _, err := hpke.KEM_X25519_HKDF_SHA256.Scheme().GenerateKeyPair()
188
+ if err != nil {
189
+ t.Fatalf("GenerateKeyPair failed: %v", err)
190
+ }
191
+
192
+ ech := &ECH{
193
+ configsMu: new(sync.RWMutex),
194
+ Publication: []*ECHPublication{
195
+ {
196
+ Domains: []string{"example.com"},
197
+ publishers: []ECHPublisher{publisher1, publisher2},
198
+ },
199
+ },
200
+ configs: map[string][]echConfig{
201
+ "example.com": {
202
+ {
203
+ PublicKey: publicKey,
204
+ Version: draftTLSESNI25,
205
+ ConfigID: 1,
206
+ RawPublicName: "example.com",
207
+ KEMID: hpke.KEM_X25519_HKDF_SHA256,
208
+ CipherSuites: []hpkeSymmetricCipherSuite{
209
+ {
210
+ KDFID: hpke.KDF_HKDF_SHA256,
211
+ AEADID: hpke.AEAD_AES128GCM,
212
+ },
213
+ },
214
+ },
215
+ },
216
+ },
217
+ }
218
+ tlsApp := &TLS{
219
+ ctx: ctx,
220
+ logger: zap.NewNop(),
221
+ EncryptedClientHello: ech,
222
+ serverNames: map[string]serverNameRegistration{
223
+ "example.com": {},
224
+ },
225
+ }
226
+
227
+ for i := 0; i < 2; i++ {
228
+ if err := tlsApp.publishECHConfigs(ctx.Context, tlsApp.logger); err != nil {
229
+ t.Fatalf("publishECHConfigs call %d failed: %v", i+1, err)
230
+ }
231
+ }
232
+
233
+ for _, publisherKey := range []string{"publisher1", "publisher2"} {
234
+ if publishCalls[publisherKey] != 1 {
235
+ t.Errorf("%s was called %d times; want once", publisherKey, publishCalls[publisherKey])
236
+ }
237
+ }
238
+
239
+ assertPublicationHistory := func(t *testing.T, history publicationHistory) {
240
+ t.Helper()
241
+ for _, publisherKey := range []string{"publisher1", "publisher2"} {
242
+ if publishedAt := history[publisherKey]["example.com"]; publishedAt.IsZero() {
243
+ t.Errorf("missing publication history for %s", publisherKey)
244
+ }
245
+ }
246
+ }
247
+
248
+ assertPublicationHistory(t, ech.configs["example.com"][0].meta.Publications)
249
+
250
+ var storedMeta echConfigMeta
251
+ if err := json.Unmarshal(storedMetadata[echMetaKey(1)], &storedMeta); err != nil {
252
+ t.Fatalf("unmarshaling stored ECH metadata: %v", err)
253
+ }
254
+ assertPublicationHistory(t, storedMeta.Publications)
255
+}
256
+
257
func TestTLSStorageCleanStopSynchronization(t *testing.T) {
258
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
259
defer cancel()
@@ -359,9 +461,6 @@ func TestTLSECHPublishBlockingPublisherStopSynchronization(t *testing.T) {
461
AEADID: hpke.AEAD_AES128GCM,
462
},
463
},
362
- meta: echConfigMeta{
363
- Publications: make(publicationHistory),
364
- },
464
},
465
},
466
},
@@ -428,8 +527,7 @@ func TestTLSECHRotateBlockingStorageStopSynchronization(t *testing.T) {
527
ConfigID: 1,
528
RawPublicName: "example.com",
529
meta: echConfigMeta{
431
- Created: time.Now().Add(-24 * time.Hour * 31), // > 30 days old so rotationNeeded returns true
432
- Publications: make(publicationHistory),
530
+ Created: time.Now().Add(-24 * time.Hour * 31), // > 30 days old so rotationNeeded returns true
531
},
532
},
533
},