@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 1,250 lines 44.4 KB
Raw
1 package caddytls
2
3 import (
4 "context"
5 "crypto/tls"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io/fs"
11 weakrand "math/rand/v2"
12 "path"
13 "strconv"
14 "strings"
15 "sync"
16 "time"
17
18 "github.com/caddyserver/certmagic"
19 "github.com/cloudflare/circl/hpke"
20 "github.com/cloudflare/circl/kem"
21 "github.com/libdns/libdns"
22 "go.uber.org/zap"
23 "golang.org/x/crypto/cryptobyte"
24
25 "github.com/caddyserver/caddy/v2"
26 )
27
28 func init() {
29 caddy.RegisterModule(ECHDNSPublisher{})
30 }
31
32 // ECH enables Encrypted ClientHello (ECH) and configures its management.
33 //
34 // ECH helps protect site names (also called "server names" or "domain names"
35 // or "SNI"), which are normally sent over plaintext when establishing a TLS
36 // connection. With ECH, the true ClientHello is encrypted and wrapped by an
37 // "outer" ClientHello that uses a more generic, shared server name that is
38 // publicly known.
39 //
40 // Clients need to know which public name (and other parameters) to use when
41 // connecting to a site with ECH, and the methods for this vary; however,
42 // major browsers support reading ECH configurations from DNS records (which
43 // is typically only secure when DNS-over-HTTPS or DNS-over-TLS is enabled in
44 // the client). Caddy has the ability to automatically publish ECH configs to
45 // DNS records if a DNS provider is configured either in the TLS app or with
46 // each individual publication config object. (Requires a custom build with a
47 // DNS provider module.)
48 //
49 // ECH requires at least TLS 1.3, so any TLS connection policies with ECH
50 // applied will automatically upgrade the minimum TLS version to 1.3, even if
51 // configured to a lower version.
52 //
53 // EXPERIMENTAL: Subject to change.
54 type ECH struct {
55 // The list of ECH configurations for which to automatically generate
56 // and rotate keys. At least one is required to enable ECH.
57 //
58 // It is strongly recommended to use as few ECH configs as possible
59 // to maximize the size of your anonymity set (see the ECH specification
60 // for a definition). Typically, each server should have only one public
61 // name, i.e. one config in this list.
62 Configs []ECHConfiguration `json:"configs,omitempty"`
63
64 // Publication describes ways to publish ECH configs for clients to
65 // discover and use. Without publication, most clients will not use
66 // ECH at all, and those that do will suffer degraded performance.
67 //
68 // Most major browsers support ECH by way of publication to HTTPS
69 // DNS RRs. (This also typically requires that they use DoH or DoT.)
70 Publication []*ECHPublication `json:"publication,omitempty"`
71
72 configsMu *sync.RWMutex // protects both configs and the list of configs/keys the standard library uses
73 configs map[string][]echConfig // map of public_name to list of configs
74 stdlibReady []tls.EncryptedClientHelloKey // ECH configs+keys in a format the standard library can use
75 }
76
77 // Provision loads or creates ECH configs and returns outer names (for certificate
78 // management), but does not publish any ECH configs. The DNS module is used as
79 // a default for later publishing if needed.
80 func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
81 ech.configsMu = new(sync.RWMutex)
82
83 logger := ctx.Logger().Named("ech")
84
85 // set up publication modules before we need to obtain a lock in storage,
86 // since this is strictly internal and doesn't require synchronization
87 for i, pub := range ech.Publication {
88 mods, err := ctx.LoadModule(pub, "PublishersRaw")
89 if err != nil {
90 return nil, fmt.Errorf("loading ECH publication modules: %v", err)
91 }
92 for _, modIface := range mods.(map[string]any) {
93 ech.Publication[i].publishers = append(ech.Publication[i].publishers, modIface.(ECHPublisher))
94 }
95 }
96
97 // the rest of provisioning needs an exclusive lock so that instances aren't
98 // stepping on each other when setting up ECH configs
99 storage := ctx.Storage()
100 if err := storage.Lock(ctx, echStorageLockName); err != nil {
101 return nil, err
102 }
103 defer func() {
104 if err := storage.Unlock(context.WithoutCancel(ctx), echStorageLockName); err != nil {
105 if !errors.Is(err, context.Canceled) && ctx.Err() == nil {
106 logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err))
107 }
108 }
109 }()
110
111 ech.configsMu.Lock()
112 defer ech.configsMu.Unlock()
113
114 outerNames, err := ech.setConfigsFromStorage(ctx, logger)
115 if err != nil {
116 return nil, fmt.Errorf("loading configs from storage: %w", err)
117 }
118
119 // see if we need to make any new ones based on the input configuration
120 for _, cfg := range ech.Configs {
121 publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName))
122
123 if list, ok := ech.configs[publicName]; !ok || len(list) == 0 {
124 // no config with this public name was loaded, so create one
125 echCfg, err := generateAndStoreECHConfig(ctx, publicName)
126 if err != nil {
127 return nil, err
128 }
129 logger.Debug("generated new ECH config",
130 zap.String("public_name", echCfg.RawPublicName),
131 zap.Uint8("id", echCfg.ConfigID))
132 ech.configs[publicName] = append(ech.configs[publicName], echCfg)
133 outerNames = append(outerNames, publicName)
134 }
135 }
136
137 // convert the configs into a structure ready for the std lib to use
138 ech.updateKeyList()
139
140 // ensure any old keys are rotated out
141 if err = ech.rotateECHKeys(ctx, logger, true); err != nil {
142 return nil, fmt.Errorf("rotating ECH configs: %w", err)
143 }
144
145 return outerNames, nil
146 }
147
148 // setConfigsFromStorage sets the ECH configs in memory to those in storage.
149 // It must be called in a write lock on ech.configsMu.
150 func (ech *ECH) setConfigsFromStorage(ctx caddy.Context, logger *zap.Logger) ([]string, error) {
151 storage := ctx.Storage()
152
153 ech.configs = make(map[string][]echConfig)
154
155 var outerNames []string
156
157 // start by loading all the existing configs (even the older ones on the way out,
158 // since some clients may still be using them if they haven't yet picked up on the
159 // new configs)
160 cfgKeys, err := storage.List(ctx, echConfigsKey, false)
161 if err != nil && !errors.Is(err, fs.ErrNotExist) { // OK if dir doesn't exist; it will be created
162 return nil, err
163 }
164 for _, cfgKey := range cfgKeys {
165 cfg, err := loadECHConfig(ctx, path.Base(cfgKey))
166 if err != nil {
167 return nil, err
168 }
169 // if any part of the config's folder was corrupted, the load function will
170 // clean it up and not return an error, since configs are immutable and
171 // fairly ephemeral... so just check that we actually got a populated config
172 if cfg.configBin == nil || cfg.privKeyBin == nil {
173 continue
174 }
175 logger.Debug("loaded ECH config",
176 zap.String("public_name", cfg.RawPublicName),
177 zap.Uint8("id", cfg.ConfigID))
178 if _, seen := ech.configs[cfg.RawPublicName]; !seen {
179 outerNames = append(outerNames, cfg.RawPublicName)
180 }
181 ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg)
182 }
183
184 return outerNames, nil
185 }
186
187 // rotateECHKeys updates the ECH keys/configs that are outdated if rotation is needed.
188 // It should be called in a write lock on ech.configsMu. If a lock is already obtained
189 // in storage, then pass true for storageSynced.
190 //
191 // This function sets/updates the stdlib-ready key list only if a rotation occurs.
192 func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSynced bool) error {
193 if err := ctx.Err(); err != nil {
194 return err
195 }
196 if ctx.Context == nil {
197 return nil
198 }
199 storage := ctx.Storage()
200 if storage == nil {
201 return nil
202 }
203
204 // all existing configs are now loaded; rotate keys "regularly" as recommended by the spec
205 // (also: "Rotating too frequently limits the client anonymity set." - but the more server
206 // names, the more frequently rotation can be done safely)
207 const (
208 rotationInterval = 24 * time.Hour * 30
209 deleteAfter = 24 * time.Hour * 90
210 )
211
212 if !ech.rotationNeeded(rotationInterval, deleteAfter) {
213 return nil
214 }
215
216 // sync this operation across cluster if not already
217 if !storageSynced {
218 if err := storage.Lock(ctx, echStorageLockName); err != nil {
219 return err
220 }
221 defer func() {
222 if err := storage.Unlock(context.WithoutCancel(ctx), echStorageLockName); err != nil {
223 if !errors.Is(err, context.Canceled) && ctx.Err() == nil {
224 logger.Error("unable to unlock ECH rotation in storage", zap.Error(err))
225 }
226 }
227 }()
228 }
229
230 // update what storage has, in case another instance already updated things
231 if _, err := ech.setConfigsFromStorage(ctx, logger); err != nil {
232 return fmt.Errorf("updating ECH keys from storage: %v", err)
233 }
234
235 // iterate the updated list and do any updates as needed
236 for publicName := range ech.configs {
237 if err := ctx.Err(); err != nil {
238 return err
239 }
240 for i := 0; i < len(ech.configs[publicName]); i++ {
241 if err := ctx.Err(); err != nil {
242 return err
243 }
244 cfg := ech.configs[publicName][i]
245 if time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero() {
246 // key is due for rotation and it hasn't been replaced yet; do that now
247 logger.Debug("ECH config is due for rotation",
248 zap.String("public_name", cfg.RawPublicName),
249 zap.Uint8("id", cfg.ConfigID),
250 zap.Time("created", cfg.meta.Created),
251 zap.Duration("age", time.Since(cfg.meta.Created)),
252 zap.Duration("rotation_interval", rotationInterval))
253
254 // start by generating and storing the replacement ECH config
255 newCfg, err := generateAndStoreECHConfig(ctx, publicName)
256 if err != nil {
257 return fmt.Errorf("generating and storing new replacement ECH config: %w", err)
258 }
259
260 // mark the key as replaced so we don't rotate it again, and instead delete it later
261 ech.configs[publicName][i].meta.Replaced = time.Now()
262
263 // persist the updated metadata
264 metaBytes, err := json.Marshal(ech.configs[publicName][i].meta)
265 if err != nil {
266 return fmt.Errorf("marshaling updated ECH config metadata: %v", err)
267 }
268 if err := storage.Store(ctx, echMetaKey(cfg.ConfigID), metaBytes); err != nil {
269 return fmt.Errorf("storing updated ECH config metadata: %v", err)
270 }
271
272 ech.configs[publicName] = append(ech.configs[publicName], newCfg)
273
274 logger.Debug("rotated ECH key",
275 zap.String("public_name", cfg.RawPublicName),
276 zap.Uint8("old_id", cfg.ConfigID),
277 zap.Uint8("new_id", newCfg.ConfigID))
278 } else if time.Since(cfg.meta.Created) >= deleteAfter && !cfg.meta.Replaced.IsZero() {
279 // key has expired and is no longer supported; delete it from storage and memory
280 cfgIDKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)))
281 if err := storage.Delete(ctx, cfgIDKey); err != nil {
282 return fmt.Errorf("deleting expired ECH config: %v", err)
283 }
284
285 ech.configs[publicName] = append(ech.configs[publicName][:i], ech.configs[publicName][i+1:]...)
286 i--
287
288 logger.Debug("deleted expired ECH key",
289 zap.String("public_name", cfg.RawPublicName),
290 zap.Uint8("id", cfg.ConfigID),
291 zap.Duration("age", time.Since(cfg.meta.Created)))
292 }
293 }
294 }
295
296 ech.updateKeyList()
297
298 return nil
299 }
300
301 // rotationNeeded returns true if any ECH key needs to be replaced, or deleted.
302 // It must be called inside a read or write lock of ech.configsMu (probably a
303 // write lock, so that the rotation can occur correctly in the same lock).)
304 func (ech *ECH) rotationNeeded(rotationInterval, deleteAfter time.Duration) bool {
305 for publicName := range ech.configs {
306 for i := 0; i < len(ech.configs[publicName]); i++ {
307 cfg := ech.configs[publicName][i]
308 if (time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero()) ||
309 (time.Since(cfg.meta.Created) >= deleteAfter && !cfg.meta.Replaced.IsZero()) {
310 return true
311 }
312 }
313 }
314 return false
315 }
316
317 // updateKeyList updates the list of ECH keys the std lib uses to serve ECH.
318 // It must be called inside a write lock on ech.configsMu.
319 func (ech *ECH) updateKeyList() {
320 ech.stdlibReady = []tls.EncryptedClientHelloKey{}
321 for _, cfgs := range ech.configs {
322 for _, cfg := range cfgs {
323 ech.stdlibReady = append(ech.stdlibReady, tls.EncryptedClientHelloKey{
324 Config: cfg.configBin,
325 PrivateKey: cfg.privKeyBin,
326 SendAsRetry: cfg.meta.Replaced.IsZero(), // only send during retries if key has not been rotated out
327 })
328 }
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 {
347 return err
348 }
349 if t.ctx.Context == nil {
350 return nil
351 }
352 // make publication exclusive, since we don't need to repeat this unnecessarily
353 storage := t.ctx.Storage()
354 if storage == nil {
355 return nil
356 }
357 const echLockName = "ech_publish"
358 if err := storage.Lock(ctx, echLockName); err != nil {
359 return err
360 }
361 defer func() {
362 if err := storage.Unlock(context.WithoutCancel(ctx), echLockName); err != nil {
363 if !errors.Is(err, context.Canceled) && ctx.Err() == nil {
364 logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err))
365 }
366 }
367 }()
368
369 // get the publication config, or use a default if not specified
370 // (the default publication config should be to publish all ECH
371 // configs to the app-global DNS provider; if no DNS provider is
372 // configured, then this whole function is basically a no-op)
373 publicationList := t.EncryptedClientHello.Publication
374 if publicationList == nil {
375 if dnsProv, ok := t.dns.(ECHDNSProvider); ok {
376 publicationList = []*ECHPublication{
377 {
378 publishers: []ECHPublisher{
379 &ECHDNSPublisher{
380 provider: dnsProv,
381 logger: logger,
382 },
383 },
384 },
385 }
386 }
387 }
388
389 // for each publication config, build the list of ECH configs to
390 // publish with it, and figure out which inner names to publish
391 // to/for, then publish
392 for _, publication := range publicationList {
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
397 var configIDs []uint8 // TODO: use IDs or the outer names?
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)
405 }
406 }
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)
414 }
415 }
416 }
417 }
418 t.EncryptedClientHello.configsMu.Unlock()
419
420 // marshal the ECH config list as binary for publication
421 echCfgListBin, err := echCfgList.MarshalBinary()
422 if err != nil {
423 return fmt.Errorf("marshaling ECH config list: %v", err)
424 }
425
426 // now we have our list of ECH configs to publish and the inner names
427 // to publish for (i.e. the names being protected); iterate each publisher
428 // and do the publish for any config+name that needs a publish
429 for _, publisher := range publication.publishers {
430 publisherKey := publisher.PublisherKey()
431
432 // by default, publish for all (non-outer) server names, unless
433 // a specific list of names is configured
434 var serverNamesSet map[string]struct{}
435 if publication.Domains == nil {
436 serverNamesSet = make(map[string]struct{}, len(t.serverNames))
437 for name := range t.serverNames {
438 // skip Tailscale names, a special case we also handle differently in our auto-HTTPS
439 if strings.HasSuffix(name, ".ts.net") {
440 continue
441 }
442 serverNamesSet[name] = struct{}{}
443 }
444 } else {
445 serverNamesSet = make(map[string]struct{}, len(publication.Domains))
446 for _, name := range publication.Domains {
447 serverNamesSet[name] = struct{}{}
448 }
449 }
450
451 // remove any domains from the set which have already had all configs in the
452 // list published by this publisher, to avoid always re-publishing unnecessarily
453 for configuredInnerName := range serverNamesSet {
454 allConfigsPublished := true
455 for _, cfg := range echCfgList {
456 // TODO: Potentially utilize the timestamp (map value) for recent-enough publication, instead of just checking for existence
457 if _, ok := cfg.meta.Publications[publisherKey][configuredInnerName]; !ok {
458 allConfigsPublished = false
459 break
460 }
461 }
462 if allConfigsPublished {
463 delete(serverNamesSet, configuredInnerName)
464 }
465 }
466
467 // if all the (inner) domains have had this ECH config list published
468 // by this publisher, then try the next publication config
469 if len(serverNamesSet) == 0 {
470 logger.Debug("ECH config list already published by publisher for associated domains (or no domains to publish for)",
471 zap.Uint8s("config_ids", configIDs),
472 zap.String("publisher", publisherKey))
473 continue
474 }
475
476 // convert the set of names to a slice
477 dnsNamesToPublish := make([]string, 0, len(serverNamesSet))
478 for name := range serverNamesSet {
479 dnsNamesToPublish = append(dnsNamesToPublish, name)
480 }
481
482 logger.Debug("publishing ECH config list",
483 zap.String("publisher", publisherKey),
484 zap.Strings("domains", dnsNamesToPublish),
485 zap.Uint8s("config_ids", configIDs))
486
487 if dnsPublisher, ok := publisher.(*ECHDNSPublisher); ok {
488 dnsPublisher.alpnByDomain = t.alpnValuesForServerNames(dnsNamesToPublish)
489 }
490
491 // publish this ECH config list with this publisher
492 pubTime := time.Now()
493 err := publisher.PublishECHConfigList(ctx, dnsNamesToPublish, echCfgListBin)
494
495 var publishErrs PublishECHConfigListErrors
496 if errors.As(err, &publishErrs) {
497 // at least a partial failure, maybe a complete failure, but we can
498 // log each error by domain
499 for innerName, domainErr := range publishErrs {
500 logger.Error("failed to publish ECH configuration list",
501 zap.String("publisher", publisherKey),
502 zap.String("domain", innerName),
503 zap.Uint8s("config_ids", configIDs),
504 zap.Error(domainErr))
505 }
506 } else if err != nil {
507 // generic error; assume the entire thing failed, I guess
508 logger.Error("failed publishing ECH configuration list",
509 zap.String("publisher", publisherKey),
510 zap.Strings("domains", dnsNamesToPublish),
511 zap.Uint8s("config_ids", configIDs),
512 zap.Error(err))
513 }
514
515 if err == nil || (len(publishErrs) > 0 && len(publishErrs) < len(dnsNamesToPublish)) {
516 // if publication for at least some domains succeeded, we should update our publication
517 // state for those domains to avoid unnecessarily republishing every time
518 someAll := "all"
519 if len(publishErrs) > 0 {
520 someAll = "some"
521 }
522 // make a list of names that published successfully with this publisher
523 // so that we update only their state in storage, not the failed ones
524 var successNames []string
525 for _, name := range dnsNamesToPublish {
526 if _, ok := publishErrs[name]; !ok {
527 successNames = append(successNames, name)
528 }
529 }
530 logger.Info("successfully published ECH configuration list for "+someAll+" domains",
531 zap.String("publisher", publisherKey),
532 zap.Strings("domains", successNames),
533 zap.Uint8s("config_ids", configIDs))
534
535 for _, cfg := range echCfgList {
536 if _, ok := cfg.meta.Publications[publisherKey]; !ok {
537 cfg.meta.Publications[publisherKey] = make(map[string]time.Time)
538 }
539 for _, name := range successNames {
540 cfg.meta.Publications[publisherKey][name] = pubTime
541 }
542 metaBytes, err := json.Marshal(cfg.meta)
543 if err != nil {
544 return fmt.Errorf("marshaling ECH config metadata: %v", err)
545 }
546 if err := t.ctx.Storage().Store(t.ctx, echMetaKey(cfg.ConfigID), metaBytes); err != nil {
547 return fmt.Errorf("storing updated ECH config metadata: %v", err)
548 }
549 }
550 } else {
551 logger.Error("all domains failed to publish ECH configuration list (see earlier errors)",
552 zap.String("publisher", publisherKey),
553 zap.Strings("domains", dnsNamesToPublish),
554 zap.Uint8s("config_ids", configIDs))
555 }
556 }
557 }
558
559 return nil
560 }
561
562 // loadECHConfig loads the config from storage with the given configID.
563 // An error is not actually returned in some cases the config fails to
564 // load because in some cases it just means the config ID folder has
565 // been cleaned up in storage, maybe due to an incomplete set of keys
566 // or corrupted contents; in any case, the only rectification is to
567 // delete it and make new keys (an error IS returned if deleting the
568 // corrupted keys fails, for example). Check the returned echConfig for
569 // non-nil privKeyBin and configBin values before using.
570 func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) {
571 storage := ctx.Storage()
572 logger := ctx.Logger()
573
574 cfgIDKey := path.Join(echConfigsKey, configID)
575 keyKey := path.Join(cfgIDKey, "key.bin")
576 configKey := path.Join(cfgIDKey, "config.bin")
577 metaKey := path.Join(cfgIDKey, "meta.json")
578
579 // if loading anything fails, might as well delete this folder and free up
580 // the config ID; spec is designed to rotate configs frequently anyway
581 // (I consider it a more serious error if we can't clean up the folder,
582 // since leaving stray storage keys is confusing)
583 privKeyBytes, err := storage.Load(ctx, keyKey)
584 if err != nil {
585 delErr := storage.Delete(ctx, cfgIDKey)
586 if delErr != nil {
587 return echConfig{}, fmt.Errorf("error loading private key (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
588 }
589 logger.Warn("could not load ECH private key; deleting its config folder",
590 zap.String("config_id", configID),
591 zap.Error(err))
592 return echConfig{}, nil
593 }
594 echConfigBytes, err := storage.Load(ctx, configKey)
595 if err != nil {
596 delErr := storage.Delete(ctx, cfgIDKey)
597 if delErr != nil {
598 return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
599 }
600 logger.Warn("could not load ECH config; deleting its config folder",
601 zap.String("config_id", configID),
602 zap.Error(err))
603 return echConfig{}, nil
604 }
605 var cfg echConfig
606 if err := cfg.UnmarshalBinary(echConfigBytes); err != nil {
607 delErr := storage.Delete(ctx, cfgIDKey)
608 if delErr != nil {
609 return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
610 }
611 logger.Warn("could not load ECH config; deleted its config folder",
612 zap.String("config_id", configID),
613 zap.Error(err))
614 return echConfig{}, nil
615 }
616 metaBytes, err := storage.Load(ctx, metaKey)
617 if errors.Is(err, fs.ErrNotExist) {
618 logger.Warn("ECH config metadata file missing; will recreate at next publication",
619 zap.String("config_id", configID),
620 zap.Error(err))
621 } else if err != nil {
622 delErr := storage.Delete(ctx, cfgIDKey)
623 if delErr != nil {
624 return echConfig{}, fmt.Errorf("error loading ECH config metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
625 }
626 logger.Warn("could not load ECH config metadata; deleted its folder",
627 zap.String("config_id", configID),
628 zap.Error(err))
629 return echConfig{}, nil
630 }
631 var meta echConfigMeta
632 if len(metaBytes) > 0 {
633 if err := json.Unmarshal(metaBytes, &meta); err != nil {
634 // even though it's just metadata, reset the whole config since we can't reliably maintain it
635 delErr := storage.Delete(ctx, cfgIDKey)
636 if delErr != nil {
637 return echConfig{}, fmt.Errorf("error decoding ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
638 }
639 logger.Warn("could not JSON-decode ECH metadata; deleted its config folder",
640 zap.String("config_id", configID),
641 zap.Error(err))
642 return echConfig{}, nil
643 }
644 }
645
646 cfg.privKeyBin = privKeyBytes
647 cfg.configBin = echConfigBytes
648 cfg.meta = meta
649
650 return cfg, nil
651 }
652
653 func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig, error) {
654 // Go currently has very strict requirements for server-side ECH configs,
655 // to quote the Go 1.24 godoc (with typos of AEAD IDs corrected):
656 //
657 // "Config should be a marshalled ECHConfig associated with PrivateKey. This
658 // must match the config provided to clients byte-for-byte. The config
659 // should only specify the DHKEM(X25519, HKDF-SHA256) KEM ID (0x0020), the
660 // HKDF-SHA256 KDF ID (0x0001), and a subset of the following AEAD IDs:
661 // AES-128-GCM (0x0001), AES-256-GCM (0x0002), ChaCha20Poly1305 (0x0003)."
662 //
663 // So we need to be sure we generate a config within these parameters
664 // so the Go TLS server can use it.
665
666 // generate a key pair
667 const kemChoice = hpke.KEM_X25519_HKDF_SHA256
668 publicKey, privateKey, err := kemChoice.Scheme().GenerateKeyPair()
669 if err != nil {
670 return echConfig{}, err
671 }
672
673 // find an available config ID
674 configID, err := newECHConfigID(ctx)
675 if err != nil {
676 return echConfig{}, fmt.Errorf("generating unique config ID: %v", err)
677 }
678
679 echCfg := echConfig{
680 PublicKey: publicKey,
681 Version: draftTLSESNI25,
682 ConfigID: configID,
683 RawPublicName: publicName,
684 KEMID: kemChoice,
685 CipherSuites: []hpkeSymmetricCipherSuite{
686 {
687 KDFID: hpke.KDF_HKDF_SHA256,
688 AEADID: hpke.AEAD_AES128GCM,
689 },
690 {
691 KDFID: hpke.KDF_HKDF_SHA256,
692 AEADID: hpke.AEAD_AES256GCM,
693 },
694 {
695 KDFID: hpke.KDF_HKDF_SHA256,
696 AEADID: hpke.AEAD_ChaCha20Poly1305,
697 },
698 },
699 }
700 meta := echConfigMeta{
701 Created: time.Now(),
702 }
703
704 privKeyBytes, err := privateKey.MarshalBinary()
705 if err != nil {
706 return echConfig{}, fmt.Errorf("marshaling ECH private key: %v", err)
707 }
708 echConfigBytes, err := echCfg.MarshalBinary()
709 if err != nil {
710 return echConfig{}, fmt.Errorf("marshaling ECH config: %v", err)
711 }
712 metaBytes, err := json.Marshal(meta)
713 if err != nil {
714 return echConfig{}, fmt.Errorf("marshaling ECH config metadata: %v", err)
715 }
716
717 parentKey := path.Join(echConfigsKey, strconv.Itoa(int(configID)))
718 keyKey := path.Join(parentKey, "key.bin")
719 configKey := path.Join(parentKey, "config.bin")
720 metaKey := path.Join(parentKey, "meta.json")
721
722 if err := ctx.Storage().Store(ctx, keyKey, privKeyBytes); err != nil {
723 return echConfig{}, fmt.Errorf("storing ECH private key: %v", err)
724 }
725 if err := ctx.Storage().Store(ctx, configKey, echConfigBytes); err != nil {
726 return echConfig{}, fmt.Errorf("storing ECH config: %v", err)
727 }
728 if err := ctx.Storage().Store(ctx, metaKey, metaBytes); err != nil {
729 return echConfig{}, fmt.Errorf("storing ECH config metadata: %v", err)
730 }
731
732 echCfg.privKeyBin = privKeyBytes
733 echCfg.configBin = echConfigBytes // this contains the public key
734 echCfg.meta = meta
735
736 return echCfg, nil
737 }
738
739 // ECH represents an Encrypted ClientHello configuration.
740 //
741 // EXPERIMENTAL: Subject to change.
742 type ECHConfiguration struct {
743 // The public server name (SNI) that will be used in the outer ClientHello.
744 // This should be a domain name for which this server is authoritative,
745 // because Caddy will try to provision a certificate for this name. As an
746 // outer SNI, it is never used for application data (HTTPS, etc.), but it
747 // is necessary for enabling clients to connect securely in some cases.
748 // If this field is empty or missing, or if Caddy cannot get a certificate
749 // for this domain (e.g. the domain's DNS records do not point to this server),
750 // client reliability becomes brittle, and you risk coercing clients to expose
751 // true server names in plaintext, which compromises both the privacy of the
752 // server and makes clients more vulnerable.
753 PublicName string `json:"public_name"`
754 }
755
756 // ECHPublication configures publication of ECH config(s). It pairs a list
757 // of ECH configs with the list of domains they are assigned to protect, and
758 // describes how to publish those configs for those domains.
759 //
760 // Most servers will have only a single publication config, unless their
761 // domains are spread across multiple DNS providers or require different
762 // methods of publication.
763 //
764 // EXPERIMENTAL: Subject to change.
765 type ECHPublication struct {
766 // The list of ECH configurations to publish, identified by public name.
767 // If not set, all configs will be included for publication by default.
768 //
769 // It is generally advised to maximize the size of your anonymity set,
770 // which implies using as few public names as possible for your sites.
771 // Usually, only a single public name is used to protect all the sites
772 // for a server
773 //
774 // EXPERIMENTAL: This field may be renamed or have its structure changed.
775 Configs []string `json:"configs,omitempty"`
776
777 // The list of ("inner") domain names which are protected with the associated
778 // ECH configurations.
779 //
780 // If not set, all server names registered with the TLS module will be
781 // added to this list implicitly. (This registration is done automatically
782 // by other Caddy apps that use the TLS module. They should register their
783 // configured server names for this purpose. For example, the HTTP server
784 // registers the hostnames for which it applies automatic HTTPS. This is
785 // not something you, the user, have to do.) Most servers
786 //
787 // Names in this list should not appear in any other publication config
788 // object with the same publishers, since the publications will likely
789 // overwrite each other.
790 //
791 // NOTE: In order to publish ECH configs for domains configured for
792 // On-Demand TLS that are not explicitly enumerated elsewhere in the
793 // config, those domain names will have to be listed here. The only
794 // time Caddy knows which domains it is serving with On-Demand TLS is
795 // handshake-time, which is too late for publishing ECH configs; it
796 // means the first connections would not protect the server names,
797 // revealing that information to observers, and thus defeating the
798 // purpose of ECH. Hence the need to list them here so Caddy can
799 // proactively publish ECH configs before clients connect with those
800 // server names in plaintext.
801 Domains []string `json:"domains,omitempty"`
802
803 // How to publish the ECH configurations so clients can know to use
804 // ECH to connect more securely to the server.
805 PublishersRaw caddy.ModuleMap `json:"publishers,omitempty" caddy:"namespace=tls.ech.publishers"`
806 publishers []ECHPublisher
807 }
808
809 // ECHDNSProvider can service DNS entries for ECH purposes.
810 type ECHDNSProvider interface {
811 libdns.RecordGetter
812 libdns.RecordSetter
813 }
814
815 // ECHDNSPublisher configures how to publish an ECH configuration to
816 // DNS records for the specified domains.
817 //
818 // EXPERIMENTAL: Subject to change.
819 type ECHDNSPublisher struct {
820 // The DNS provider module which will establish the HTTPS record(s).
821 ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
822 provider ECHDNSProvider
823
824 alpnByDomain map[string][]string
825 logger *zap.Logger
826 }
827
828 // CaddyModule returns the Caddy module information.
829 func (ECHDNSPublisher) CaddyModule() caddy.ModuleInfo {
830 return caddy.ModuleInfo{
831 ID: "tls.ech.publishers.dns",
832 New: func() caddy.Module { return new(ECHDNSPublisher) },
833 }
834 }
835
836 func (dnsPub *ECHDNSPublisher) Provision(ctx caddy.Context) error {
837 dnsProvMod, err := ctx.LoadModule(dnsPub, "ProviderRaw")
838 if err != nil {
839 return fmt.Errorf("loading ECH DNS provider module: %v", err)
840 }
841 prov, ok := dnsProvMod.(ECHDNSProvider)
842 if !ok {
843 return fmt.Errorf("ECH DNS provider module is not an ECH DNS Provider: %v", err)
844 }
845 dnsPub.provider = prov
846 dnsPub.logger = ctx.Logger()
847 return nil
848 }
849
850 // PublisherKey returns the name of the DNS provider module.
851 // We intentionally omit specific provider configuration (or a hash thereof,
852 // since the config is likely sensitive, potentially containing an API key)
853 // because it is unlikely that specific configuration, such as an API key,
854 // is relevant to unique key use as an ECH config publisher.
855 func (dnsPub ECHDNSPublisher) PublisherKey() string {
856 return string(dnsPub.provider.(caddy.Module).CaddyModule().ID)
857 }
858
859 // PublishECHConfigList publishes the given ECH config list (as binary) to the given DNS names.
860 // If there is an error, it may be of type PublishECHConfigListErrors, detailing
861 // potentially multiple errors keyed by associated innerName.
862 func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, configListBin []byte) error {
863 nameservers := certmagic.RecursiveNameservers(nil) // TODO: we could make resolvers configurable
864
865 errs := make(PublishECHConfigListErrors)
866
867 nextName:
868 for _, domain := range innerNames {
869 zone, err := certmagic.FindZoneByFQDN(ctx, dnsPub.logger, domain, nameservers)
870 if err != nil {
871 errs[domain] = fmt.Errorf("could not determine zone for domain: %w (domain=%s nameservers=%v)", err, domain, nameservers)
872 continue
873 }
874
875 relName := libdns.RelativeName(domain+".", zone)
876
877 // get existing records for this domain; we need to make sure another
878 // record exists for it so we don't accidentally trample a wildcard; we
879 // also want to get any HTTPS record that may already exist for it so
880 // we can augment the ech SvcParamKey with any other existing SvcParams
881 recs, err := dnsPub.provider.GetRecords(ctx, zone)
882 if err != nil {
883 errs[domain] = fmt.Errorf("unable to get existing DNS records to publish ECH data to HTTPS DNS record: %w", err)
884 continue
885 }
886 var httpsRec libdns.ServiceBinding
887 var nameHasExistingRecord bool
888 for _, rec := range recs {
889 rr := rec.RR()
890 if rr.Name == relName {
891 // CNAME records are exclusive of all other records, so we cannot publish an HTTPS
892 // record for a domain that is CNAME'd. See #6922.
893 if rr.Type == "CNAME" {
894 dnsPub.logger.Warn("domain has CNAME record, so unable to publish ECH data to HTTPS record",
895 zap.String("domain", domain),
896 zap.String("cname_value", rr.Data))
897 continue nextName
898 }
899 nameHasExistingRecord = true
900 if svcb, ok := rec.(libdns.ServiceBinding); ok && svcb.Scheme == "https" {
901 if svcb.Target == "" || svcb.Target == "." {
902 httpsRec = svcb
903 break
904 }
905 }
906 }
907 }
908 if !nameHasExistingRecord {
909 // Turns out if you publish a DNS record for a name that doesn't have any DNS record yet,
910 // any wildcard records won't apply for the name anymore, meaning if a wildcard A/AAAA record
911 // is used to resolve the domain to a server, publishing an HTTPS record could break resolution!
912 // In theory, this should be a non-issue, at least for A/AAAA records, if the HTTPS record
913 // includes ipv[4|6]hint SvcParamKeys,
914 dnsPub.logger.Warn("domain does not have any existing records, so skipping publication of HTTPS record",
915 zap.String("domain", domain),
916 zap.String("relative_name", relName),
917 zap.String("zone", zone))
918 continue
919 }
920 params := httpsRec.Params
921 params = dnsPub.publishedSvcParams(domain, params, configListBin)
922
923 // publish record
924 _, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
925 libdns.ServiceBinding{
926 // HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460)
927 Scheme: "https",
928 Name: relName,
929 TTL: 5 * time.Minute, // TODO: low hard-coded value only temporary; change to a higher value once more field-tested and key rotation is implemented
930 Priority: 2, // allows a manual override with priority 1
931 Target: ".",
932 Params: params,
933 },
934 })
935 if err != nil {
936 errs[domain] = fmt.Errorf("unable to publish ECH data to HTTPS DNS record: %w (zone=%s dns_record_name=%s)", err, zone, relName)
937 continue
938 }
939 }
940
941 if len(errs) > 0 {
942 return errs
943 }
944 return nil
945 }
946
947 func (dnsPub *ECHDNSPublisher) publishedSvcParams(domain string, existing libdns.SvcParams, configListBin []byte) libdns.SvcParams {
948 params := make(libdns.SvcParams, len(existing)+2)
949 for key, values := range existing {
950 params[key] = append([]string(nil), values...)
951 }
952
953 params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
954
955 if len(dnsPub.alpnByDomain) == 0 {
956 return params
957 }
958
959 if alpn := dnsPub.alpnByDomain[strings.ToLower(domain)]; len(alpn) > 0 {
960 params["alpn"] = append([]string(nil), alpn...)
961 }
962
963 return params
964 }
965
966 // echConfig represents an ECHConfig from the specification,
967 // [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html).
968 type echConfig struct {
969 // "The version of ECH for which this configuration is used.
970 // The version is the same as the code point for the
971 // encrypted_client_hello extension. Clients MUST ignore any
972 // ECHConfig structure with a version they do not support."
973 Version uint16
974
975 // The "length" and "contents" fields defined next in the
976 // structure are implicitly taken care of by cryptobyte
977 // when encoding the following fields:
978
979 // HpkeKeyConfig fields:
980 ConfigID uint8
981 KEMID hpke.KEM
982 PublicKey kem.PublicKey
983 CipherSuites []hpkeSymmetricCipherSuite
984
985 // ECHConfigContents fields:
986 MaxNameLength uint8
987 RawPublicName string
988 RawExtensions []byte
989
990 // these fields are not part of the spec, but are here for
991 // our use when setting up TLS servers or maintenance
992 configBin []byte
993 privKeyBin []byte
994 meta echConfigMeta
995 }
996
997 func (echCfg echConfig) MarshalBinary() ([]byte, error) {
998 var b cryptobyte.Builder
999 if err := echCfg.marshalBinary(&b); err != nil {
1000 return nil, err
1001 }
1002 return b.Bytes()
1003 }
1004
1005 // UnmarshalBinary decodes the data back into an ECH config.
1006 //
1007 // Borrowed from github.com/OmarTariq612/goech with modifications.
1008 // Original code: Copyright (c) 2023 Omar Tariq AbdEl-Raziq
1009 func (echCfg *echConfig) UnmarshalBinary(data []byte) error {
1010 var content cryptobyte.String
1011 b := cryptobyte.String(data)
1012
1013 if !b.ReadUint16(&echCfg.Version) {
1014 return errInvalidLen
1015 }
1016 if echCfg.Version != draftTLSESNI25 {
1017 return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI25, echCfg.Version)
1018 }
1019
1020 if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() {
1021 return errInvalidLen
1022 }
1023
1024 var t cryptobyte.String
1025 var pk []byte
1026
1027 if !content.ReadUint8(&echCfg.ConfigID) ||
1028 !content.ReadUint16((*uint16)(&echCfg.KEMID)) ||
1029 !content.ReadUint16LengthPrefixed(&t) ||
1030 !t.ReadBytes(&pk, len(t)) ||
1031 !content.ReadUint16LengthPrefixed(&t) ||
1032 len(t)%4 != 0 /* the length of (KDFs and AEADs) must be divisible by 4 */ {
1033 return errInvalidLen
1034 }
1035
1036 if !echCfg.KEMID.IsValid() {
1037 return fmt.Errorf("invalid KEM ID: %d", echCfg.KEMID)
1038 }
1039
1040 var err error
1041 if echCfg.PublicKey, err = echCfg.KEMID.Scheme().UnmarshalBinaryPublicKey(pk); err != nil {
1042 return fmt.Errorf("parsing public_key: %w", err)
1043 }
1044
1045 echCfg.CipherSuites = echCfg.CipherSuites[:0]
1046
1047 for !t.Empty() {
1048 var hpkeKDF, hpkeAEAD uint16
1049 if !t.ReadUint16(&hpkeKDF) || !t.ReadUint16(&hpkeAEAD) {
1050 // we have already checked that the length is divisible by 4
1051 panic("this must not happen")
1052 }
1053 if !hpke.KDF(hpkeKDF).IsValid() {
1054 return fmt.Errorf("invalid KDF ID: %d", hpkeKDF)
1055 }
1056 if !hpke.AEAD(hpkeAEAD).IsValid() {
1057 return fmt.Errorf("invalid AEAD ID: %d", hpkeAEAD)
1058 }
1059 echCfg.CipherSuites = append(echCfg.CipherSuites, hpkeSymmetricCipherSuite{
1060 KDFID: hpke.KDF(hpkeKDF),
1061 AEADID: hpke.AEAD(hpkeAEAD),
1062 })
1063 }
1064
1065 var rawPublicName []byte
1066 if !content.ReadUint8(&echCfg.MaxNameLength) ||
1067 !content.ReadUint8LengthPrefixed(&t) ||
1068 !t.ReadBytes(&rawPublicName, len(t)) ||
1069 !content.ReadUint16LengthPrefixed(&t) ||
1070 !t.ReadBytes(&echCfg.RawExtensions, len(t)) ||
1071 !content.Empty() {
1072 return errInvalidLen
1073 }
1074 echCfg.RawPublicName = string(rawPublicName)
1075
1076 return nil
1077 }
1078
1079 var errInvalidLen = errors.New("invalid length")
1080
1081 // marshalBinary writes this config to the cryptobyte builder. If there is an error,
1082 // it will occur before any writes have happened.
1083 func (echCfg echConfig) marshalBinary(b *cryptobyte.Builder) error {
1084 pk, err := echCfg.PublicKey.MarshalBinary()
1085 if err != nil {
1086 return err
1087 }
1088 if l := len(echCfg.RawPublicName); l == 0 || l > 255 {
1089 return fmt.Errorf("public name length (%d) must be in the range 1-255", l)
1090 }
1091
1092 b.AddUint16(echCfg.Version)
1093 b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { // "length" field
1094 b.AddUint8(echCfg.ConfigID)
1095 b.AddUint16(uint16(echCfg.KEMID))
1096 b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
1097 b.AddBytes(pk)
1098 })
1099 b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
1100 for _, cs := range echCfg.CipherSuites {
1101 b.AddUint16(uint16(cs.KDFID))
1102 b.AddUint16(uint16(cs.AEADID))
1103 }
1104 })
1105 b.AddUint8(uint8(min(len(echCfg.RawPublicName)+16, 255)))
1106 b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
1107 b.AddBytes([]byte(echCfg.RawPublicName))
1108 })
1109 b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
1110 child.AddBytes(echCfg.RawExtensions)
1111 })
1112 })
1113
1114 return nil
1115 }
1116
1117 type hpkeSymmetricCipherSuite struct {
1118 KDFID hpke.KDF
1119 AEADID hpke.AEAD
1120 }
1121
1122 type echConfigList []echConfig
1123
1124 func (cl echConfigList) MarshalBinary() ([]byte, error) {
1125 var b cryptobyte.Builder
1126 var err error
1127
1128 // the list's length prefixes the list, as with most opaque values
1129 b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
1130 for _, cfg := range cl {
1131 if err = cfg.marshalBinary(b); err != nil {
1132 break
1133 }
1134 }
1135 })
1136 if err != nil {
1137 return nil, err
1138 }
1139
1140 return b.Bytes()
1141 }
1142
1143 func newECHConfigID(ctx caddy.Context) (uint8, error) {
1144 // uint8 can be 0-255 inclusive
1145 const uint8Range = 256
1146
1147 // avoid repeating storage checks
1148 tried := make([]bool, uint8Range)
1149
1150 // Try to find an available number with random rejection sampling;
1151 // i.e. choose a random number and see if it's already taken.
1152 // The hard limit on how many times we try to find an available
1153 // number is flexible... in theory, assuming uniform distribution,
1154 // 256 attempts should make each possible value show up exactly
1155 // once, but obviously that won't be the case. We can try more
1156 // times to try to ensure that every number gets a chance, which
1157 // is especially useful if few are available, or we can lower it
1158 // if we assume we should have found an available value by then
1159 // and want to limit runtime; for now I choose the middle ground
1160 // and just try as many times as there are possible values.
1161 for i := 0; i < uint8Range && ctx.Err() == nil; i++ {
1162 num := uint8(weakrand.N(uint8Range)) //nolint:gosec
1163
1164 // don't try the same number a second time
1165 if tried[num] {
1166 continue
1167 }
1168 tried[num] = true
1169
1170 // check to see if any of the subkeys use this config ID
1171 numStr := strconv.Itoa(int(num))
1172 trialPath := path.Join(echConfigsKey, numStr)
1173 if ctx.Storage().Exists(ctx, trialPath) {
1174 continue
1175 }
1176
1177 return num, nil
1178 }
1179
1180 if err := ctx.Err(); err != nil {
1181 return 0, err
1182 }
1183
1184 return 0, fmt.Errorf("depleted attempts to find an available config_id")
1185 }
1186
1187 // ECHPublisher is an interface for publishing ECHConfigList values
1188 // so that they can be used by clients.
1189 type ECHPublisher interface {
1190 // Returns a key that is unique to this publisher and its configuration.
1191 // A publisher's ID combined with its config is a valid key.
1192 // It is used to prevent duplicating publications.
1193 PublisherKey() string
1194
1195 // Publishes the ECH config list (as binary) for the given innerNames. Some
1196 // publishers may not need a list of inner/protected names, and can ignore the
1197 // argument; most, however, will want to use it to know which inner names are
1198 // to be associated with the given ECH config list.
1199 //
1200 // Implementations should return an error of type PublishECHConfigListErrors
1201 // when relevant to key errors to their associated innerName, but should never
1202 // return a non-nil PublishECHConfigListErrors when its length is 0.
1203 PublishECHConfigList(ctx context.Context, innerNames []string, echConfigList []byte) error
1204 }
1205
1206 // PublishECHConfigListErrors is returned by ECHPublishers to describe one or more
1207 // errors publishing an ECH config list from PublishECHConfigList. A non-nil, empty
1208 // value of this type should never be returned.
1209 // nolint:errname // The linter wants "Error" convention, but this is a multi-error type.
1210 type PublishECHConfigListErrors map[string]error
1211
1212 func (p PublishECHConfigListErrors) Error() string {
1213 var sb strings.Builder
1214 for innerName, err := range p {
1215 if sb.Len() > 0 {
1216 sb.WriteString("; ")
1217 }
1218 sb.WriteString(innerName)
1219 sb.WriteString(": ")
1220 sb.WriteString(err.Error())
1221 }
1222 return sb.String()
1223 }
1224
1225 type echConfigMeta struct {
1226 Created time.Time `json:"created"`
1227 Replaced time.Time `json:"replaced,omitzero"`
1228 Publications publicationHistory `json:"publications"`
1229 }
1230
1231 func echMetaKey(configID uint8) string {
1232 return path.Join(echConfigsKey, strconv.Itoa(int(configID)), "meta.json")
1233 }
1234
1235 // publicationHistory is a map of publisher key to
1236 // map of inner name to timestamp
1237 type publicationHistory map[string]map[string]time.Time
1238
1239 // echStorageLockName is the name of the storage lock to sync ECH updates.
1240 const echStorageLockName = "ech_rotation"
1241
1242 // The key prefix when putting ECH configs in storage. After this
1243 // comes the config ID.
1244 const echConfigsKey = "ech/configs"
1245
1246 // https://www.ietf.org/archive/id/draft-ietf-tls-esni-25.html
1247 const draftTLSESNI25 = 0xfe0d
1248
1249 // Interface guard
1250 var _ ECHPublisher = (*ECHDNSPublisher)(nil)