Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ range may break in any release.

### Added

- **Live PQC handshake gate met (RELEASING.md gate #2).** Added
`live_handshake_negotiates_x25519mlkem768`, an `#[ignore]`d interop test that
drives a real TLS 1.3 handshake with the `pqc` client config against
Cloudflare's PQC host (`pq.cloudflareresearch.com`) and asserts the
*negotiated* group is X25519MLKEM768 — confirming the hand-rolled hybrid wire
layout interoperates with an independent server, not just our own halves. Run
with `cargo test -p vault-api --features pqc -- --ignored live_handshake`
(`docs/pqc.md`). Second of the v0.1 operational gates cleared; only the §11.2
two-week daily-driver attestation remains.

- **EncString fuzz soak passed (PRD §11.4 / RELEASING.md gate #1).** A ≥ 24 h
libFuzzer soak of the `EncString` type-2 parser completed with **0 findings**:
8,874,210,317 executions over 86,401 s (~102.7 k exec/s), coverage flat at 312
Expand Down
5 changes: 4 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ do the mechanical cut below.
(see `docs/fuzzing.md`). Any reproducer under `fuzz/artifacts/` blocks the
tag until fixed. **Done 2026-06-18:** 8.87 B executions over 86,401 s, 0
findings, exit 0 — full run logged in `docs/fuzzing-report.md`.
- [ ] **Live PQC handshake** — build with PQC and confirm an X25519MLKEM768
- [x] **Live PQC handshake** — build with PQC and confirm an X25519MLKEM768
handshake against a PQC-enabled endpoint:
`cargo build -p vault-agent --features pqc` (see `docs/pqc.md`).
**Done 2026-06-18:** `live_handshake_negotiates_x25519mlkem768`
(`cargo test -p vault-api --features pqc -- --ignored live_handshake`)
negotiated X25519MLKEM768 against `pq.cloudflareresearch.com`.
- [ ] **End-to-end** (PRD §11.1) — `register` / `login` / `sync` / `get` against
both bitwarden.com and a Vaultwarden container (`docs/m2-vaultwarden.md`).
- [ ] **Daily-driver** (PRD §11.2) — two consecutive weeks of maintainer use with
Expand Down
39 changes: 39 additions & 0 deletions crates/vault-api/src/pqc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,43 @@ mod tests {
.expect("at least one kx group");
assert_eq!(first.name(), NamedGroup::X25519MLKEM768);
}

/// Live gate (PRD §11.4 / `RELEASING.md`): drive a real TLS 1.3 handshake
/// with the [`client_config`] against a PQC-enabled server and confirm the
/// hybrid group is actually *negotiated*, not merely offered. Cloudflare's
/// research host supports X25519MLKEM768; the unit tests above only exercise
/// our half of the exchange, so this is the one check that the wire layout
/// interoperates with an independent server implementation.
///
/// `#[ignore]`d — needs network. Run with:
/// `cargo test -p vault-api --features pqc -- --ignored live_handshake`.
#[test]
#[ignore = "live network: handshakes with pq.cloudflareresearch.com:443"]
fn live_handshake_negotiates_x25519mlkem768() {
use rustls::pki_types::ServerName;
use std::net::TcpStream;

const HOST: &str = "pq.cloudflareresearch.com";

let config = Arc::new(client_config().expect("config builds"));
let server_name = ServerName::try_from(HOST).expect("valid server name");
let mut conn =
rustls::ClientConnection::new(config, server_name).expect("client connection");
let mut sock = TcpStream::connect((HOST, 443)).expect("tcp connect");

// Drive ClientHello → ServerHello…Finished → client Finished to
// completion; no application data needed to fix the negotiated group.
conn.complete_io(&mut sock)
.expect("tls handshake completes");

let group = conn
.negotiated_key_exchange_group()
.expect("a key-exchange group was negotiated");
assert_eq!(
group.name(),
NamedGroup::X25519MLKEM768,
"server negotiated {:?}, not the hybrid PQC group",
group.name(),
);
}
}
23 changes: 19 additions & 4 deletions docs/pqc.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ When enabled, the client offers X25519MLKEM768 first and the classical groups
don't yet — the handshake silently falls back to a classical group, so enabling
the feature is safe. PQC is TLS 1.3 only.

## Live handshake test

The unit tests in `crates/vault-api/src/pqc.rs` exercise only our half of the
exchange (KEM round-trip, share/secret layout, config ordering). The
`live_handshake_negotiates_x25519mlkem768` test is the interop gate: it drives a
real TLS 1.3 handshake with [`client_config`] against Cloudflare's PQC research
host (`pq.cloudflareresearch.com`) and asserts the *negotiated* key-exchange
group is X25519MLKEM768 — proving the wire construction interoperates with an
independent server. It is `#[ignore]`d (needs network); run it with:

```sh
cargo test -p vault-api --features pqc -- --ignored live_handshake
```

## Why we hand-roll it (and don't use aws-lc-rs)

rustls only ships X25519MLKEM768 through its **aws-lc-rs** provider. aws-lc-rs
Expand Down Expand Up @@ -53,9 +67,10 @@ concatenates the two secrets (PQ first). This mirrors rustls's own

## Status (PRD §12 M7)

This satisfies the M7 "PQC transport feature flag" item. Still pending for the
`v0.1` tag: making PQC a tested live handshake against a PQC-enabled server, the
≥ 24 h EncString fuzz soak (`docs/fuzzing.md`), the broader hardening pass, and
the §11.2 two-week daily-driver attestation.
This satisfies the M7 "PQC transport feature flag" item, and the live handshake
gate is now met (see above — `live_handshake_negotiates_x25519mlkem768` confirms
X25519MLKEM768 against Cloudflare). The ≥ 24 h EncString fuzz soak is also done
(`docs/fuzzing-report.md`). Still pending for the `v0.1` tag: the §11.2 two-week
daily-driver attestation.

[`ml-kem`]: https://crates.io/crates/ml-kem
Loading