@marcel91200 / caddy Mirror · read-only / commits / daa74db1
Fork 0

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 },