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
| 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) |