rotateECHKeys updates the ECH keys/configs that are outdated if rotation is needed. It should be called in a write lock on ech.configsMu. If a lock is already obtained in storage, then pass true for storageSynced. This function sets/updates the stdlib-ready key list only if a rotation occurs.
(ctx caddy.Context, logger *zap.Logger, storageSynced bool)
| 188 | // |
| 189 | // This function sets/updates the stdlib-ready key list only if a rotation occurs. |
| 190 | func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSynced bool) error { |
| 191 | storage := ctx.Storage() |
| 192 | |
| 193 | // all existing configs are now loaded; rotate keys "regularly" as recommended by the spec |
| 194 | // (also: "Rotating too frequently limits the client anonymity set." - but the more server |
| 195 | // names, the more frequently rotation can be done safely) |
| 196 | const ( |
| 197 | rotationInterval = 24 * time.Hour * 30 |
| 198 | deleteAfter = 24 * time.Hour * 90 |
| 199 | ) |
| 200 | |
| 201 | if !ech.rotationNeeded(rotationInterval, deleteAfter) { |
| 202 | return nil |
| 203 | } |
| 204 | |
| 205 | // sync this operation across cluster if not already |
| 206 | if !storageSynced { |
| 207 | if err := storage.Lock(ctx, echStorageLockName); err != nil { |
| 208 | return err |
| 209 | } |
| 210 | defer func() { |
| 211 | if err := storage.Unlock(ctx, echStorageLockName); err != nil { |
| 212 | logger.Error("unable to unlock ECH rotation in storage", zap.Error(err)) |
| 213 | } |
| 214 | }() |
| 215 | } |
| 216 | |
| 217 | // update what storage has, in case another instance already updated things |
| 218 | if _, err := ech.setConfigsFromStorage(ctx, logger); err != nil { |
| 219 | return fmt.Errorf("updating ECH keys from storage: %v", err) |
| 220 | } |
| 221 | |
| 222 | // iterate the updated list and do any updates as needed |
| 223 | for publicName := range ech.configs { |
| 224 | for i := 0; i < len(ech.configs[publicName]); i++ { |
| 225 | cfg := ech.configs[publicName][i] |
| 226 | if time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero() { |
| 227 | // key is due for rotation and it hasn't been replaced yet; do that now |
| 228 | logger.Debug("ECH config is due for rotation", |
| 229 | zap.String("public_name", cfg.RawPublicName), |
| 230 | zap.Uint8("id", cfg.ConfigID), |
| 231 | zap.Time("created", cfg.meta.Created), |
| 232 | zap.Duration("age", time.Since(cfg.meta.Created)), |
| 233 | zap.Duration("rotation_interval", rotationInterval)) |
| 234 | |
| 235 | // start by generating and storing the replacement ECH config |
| 236 | newCfg, err := generateAndStoreECHConfig(ctx, publicName) |
| 237 | if err != nil { |
| 238 | return fmt.Errorf("generating and storing new replacement ECH config: %w", err) |
| 239 | } |
| 240 | |
| 241 | // mark the key as replaced so we don't rotate it again, and instead delete it later |
| 242 | ech.configs[publicName][i].meta.Replaced = time.Now() |
| 243 | |
| 244 | // persist the updated metadata |
| 245 | metaBytes, err := json.Marshal(ech.configs[publicName][i].meta) |
| 246 | if err != nil { |
| 247 | return fmt.Errorf("marshaling updated ECH config metadata: %v", err) |
no test coverage detected