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:
@@ -16,7 +16,7 @@ mod store;
|
||||
|
||||
pub use ca::{AuraCa, IssuedCert};
|
||||
pub use cert::AuraCertVerifier;
|
||||
pub use store::CrlStore;
|
||||
pub use store::{sign_ecdsa_p256, verify_ecdsa_p256, CrlStore};
|
||||
|
||||
/// Errors produced by the Aura PKI.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -156,10 +156,7 @@ impl CrlStore {
|
||||
let sig_text = text[idx + marker.len()..].trim();
|
||||
let signature = hex_decode(sig_text).context("decoding signed CRL hex signature")?;
|
||||
|
||||
let pubkey = ca_public_key_from_pem(ca_cert_pem)
|
||||
.context("loading CA public key for CRL verification")?;
|
||||
UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, pubkey.as_slice())
|
||||
.verify(body.as_bytes(), &signature)
|
||||
verify_ecdsa_p256(ca_cert_pem, body.as_bytes(), &signature)
|
||||
.map_err(|_| anyhow!("signed CRL signature did not verify"))?;
|
||||
|
||||
// Parse the inner body. Skip the magic line, then keep non-empty / non-comment lines.
|
||||
@@ -207,7 +204,10 @@ const SIGNATURE_MARKER: &[u8] = b"--SIGNATURE--\n";
|
||||
|
||||
/// Sign `body` with an ECDSA-P256/SHA-256 PKCS#8 key (PEM-encoded). Returns the ASN.1 signature
|
||||
/// bytes (variable-length DER) that `ring::signature::ECDSA_P256_SHA256_ASN1` accepts on verify.
|
||||
fn sign_ecdsa_p256(ca_key_pem: &str, body: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
///
|
||||
/// Exposed publicly so the v3.3 signed-bridges manifest in `aura-cli` reuses the same signing
|
||||
/// primitive as the in-band CRL push (consistent on-disk format and signature algorithm).
|
||||
pub fn sign_ecdsa_p256(ca_key_pem: &str, body: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
let pkcs8_der = pem_block_to_der(ca_key_pem, &["PRIVATE KEY", "EC PRIVATE KEY"])
|
||||
.ok_or_else(|| anyhow!("no PKCS#8 private-key block in CA key PEM"))?;
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
@@ -219,6 +219,19 @@ fn sign_ecdsa_p256(ca_key_pem: &str, body: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
Ok(sig.as_ref().to_vec())
|
||||
}
|
||||
|
||||
/// Verify an ECDSA-P256/SHA-256 ASN.1 signature against a CA certificate PEM.
|
||||
///
|
||||
/// Exposed publicly so the v3.3 signed-bridges manifest in `aura-cli` shares the same verification
|
||||
/// primitive as the in-band CRL push. Returns `Err` when the CA PEM cannot be parsed or when the
|
||||
/// signature does not validate.
|
||||
pub fn verify_ecdsa_p256(ca_cert_pem: &str, body: &[u8], signature: &[u8]) -> anyhow::Result<()> {
|
||||
let pubkey = ca_public_key_from_pem(ca_cert_pem)
|
||||
.context("loading CA public key for signature verification")?;
|
||||
UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, pubkey.as_slice())
|
||||
.verify(body, signature)
|
||||
.map_err(|_| anyhow!("ECDSA-P256/SHA-256 signature did not verify"))
|
||||
}
|
||||
|
||||
/// Extract the CA's uncompressed EC public-key point from a CA certificate PEM.
|
||||
fn ca_public_key_from_pem(ca_cert_pem: &str) -> anyhow::Result<Vec<u8>> {
|
||||
let der = pem_block_to_der(ca_cert_pem, &["CERTIFICATE"])
|
||||
|
||||
Reference in New Issue
Block a user