feat(cli,pki): v3.3 bridge discovery via signed CA manifest

Closes the v3.3 "bridges by hand" honest limitation. Admins now publish a
CA-signed manifest with the current bridge list; clients re-read it from
disk on a timer and merge it with the static [client] bridges. Cuts the
"rotate the bridge list" cycle from "edit every client config" to
"distribute one signed file".

- New aura sign-bridges CLI:
    aura sign-bridges --ca /etc/aura/pki \
                      --bridges "ip1:443,ip2:443" \
                      --ttl-days 7 \
                      --out /var/aura/bridges.signed
- Manifest format (single file, text + signature block, same shape as the
  in-band CRL):
    AURA-BRIDGES-v1
    {"version":1,"generated_at":...,"expires_at":...,"bridges":[...]}
    --SIGNATURE--
    <hex ECDSA-P256/SHA-256 over body>
- aura-pki now exports `sign_ecdsa_p256` / `verify_ecdsa_p256` so CRL and
  bridges share ONE signing primitive (no copy-paste). CRL keeps working.
- aura-cli::bridges::BridgeManifest + BridgesDiscoveryWatcher: new
  module. encode_signed/load_signed_verified verifies signature + rejects
  expired manifests. Watcher spawns a tokio interval that re-reads the
  file; on load failure (truncated, expired, bad sig) the previous
  snapshot is kept — bridges never collapse to empty.
- New [client.bridges_discovery] {enabled, manifest_path,
  refresh_interval_secs}; serde(default) so v3.2 configs keep working.
- Merge strategy: manifest EXTENDS static [client] bridges, dedup by
  SocketAddr, static-first ordering. Static remains as fallback.
- 13 new tests (8 lib unit + 4 integration + 1 config). Workspace: 310
  tests passed (+13), clippy -D warnings clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-27 21:39:23 +03:00
parent 5e553b79df
commit a173ced9b2
9 changed files with 1023 additions and 13 deletions
+33
View File
@@ -465,6 +465,39 @@ pub struct ClientSection {
/// Living inside `[client]` matches the TOML path operators write: `[client.circuit]`.
#[serde(default)]
pub circuit: CircuitSection,
/// `[client.bridges_discovery]` sub-section: v3.3 CA-signed bridges manifest. When
/// `enabled = true`, the client periodically reloads a signed manifest from
/// `manifest_path` and merges the resulting bridge list with the static
/// `[client] bridges` baseline. See [`crate::bridges`]. Default `enabled = false`
/// (back-compat — the static list is used verbatim).
#[serde(default)]
pub bridges_discovery: BridgesDiscoverySection,
}
/// `[client.bridges_discovery]` section: v3.3 signed bridges manifest configuration. See
/// [`crate::bridges::BridgeManifest`] for the wire format and [`crate::bridges::BridgesDiscoveryWatcher`]
/// for the runtime behaviour.
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct BridgesDiscoverySection {
/// Master switch. `false` (the default) keeps the v3.2 behaviour where `[client] bridges` is
/// the only source. `true` enables the watcher.
pub enabled: bool,
/// File path of the signed manifest on disk. Path may begin with `~`. REQUIRED when `enabled`.
pub manifest_path: PathBuf,
/// Refresh cadence in seconds. The watcher reloads the file every `refresh_interval_secs`
/// (defaults to 3600 = one hour). Zero disables the background timer (one-shot load).
pub refresh_interval_secs: u64,
}
impl Default for BridgesDiscoverySection {
fn default() -> Self {
Self {
enabled: false,
manifest_path: PathBuf::new(),
refresh_interval_secs: 3600,
}
}
}
/// `[tunnel]` section of `client.toml`.