aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-13 12:08:48 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-13 12:08:48 +0200
commit197f5e25893b845995853379125f607be18fc4e9 (patch)
tree1734dd67e3e58f0af0574dd37518c21d26985cf9
parentf4bbc090d7e840c5dc7a838289441955cbdb7fd5 (diff)
downloadmeshbay-197f5e25893b845995853379125f607be18fc4e9.tar.gz
docs: record 11.5.6 spike — QUIC channel binding constraints
Investigated aioquic 1.3.0 before implementing the QUIC challenge/response, since the binding anchor gates the whole design. No RFC 5705 exporter exists (aioquic.tls.Context has no export_keying_material), so the preferred anchor is unavailable. Certificate access is asymmetric: the server reaches its own cert via the public tls.certificate, but the client can only reach the server's via tls._peer_certificate — a private attribute, behind a QuicConnection that exposes no tls accessor at all. That matters because binding a security check to a private API means an upgrade can remove it silently. Since make_proof() refuses an empty binding (11.5.21), a rename would fail loudly rather than degrade — but only while the refusal path stays strict. Three options recorded with a recommendation: certificate hash via the private attribute with a guard test that fails CI on upgrade, plus pinned aioquic; or bind to pk_node instead, which for QUIC may suffice since signaling is not hub-relayed — but that requires certificate pinning, as the QUIC client currently does not verify the TLS certificate at all; or upstream an exporter. No implementation started: the challenge/response needs both protocol sides, test updates in two files and multiple verification cycles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--devel-phases-next.md40
1 files changed, 39 insertions, 1 deletions
diff --git a/devel-phases-next.md b/devel-phases-next.md
index 687e4f1..902f01d 100644
--- a/devel-phases-next.md
+++ b/devel-phases-next.md
@@ -614,7 +614,45 @@ phase reduces the node to two paths and brings both to the same standard.
|---|---|---|---|
| 11.5.4 | Extract `meshbay_common/handshake.py`: JWT verify → `scope == "user"` → denylist → **mandatory** `group_id` in claims → group hosted → GEK challenge → proof verify → ack | **C6**, M1, M9 | Single implementation; `webrtc_server.py` and `quic_server.py` contain no JWT logic of their own |
| 11.5.5 | Both transports call it; test parametrized over `[webrtc, quic]` | C6 | A test that adds a step to the handshake fails for any transport that skips it |
-| 11.5.6 | **Spike:** channel binding for QUIC. No DTLS fingerprint exists — bind to the QUIC server certificate hash as the analogue (`sha256(server_cert) ‖ sha256(client_cert)`); prefer an RFC 5705 TLS exporter if `aioquic` can expose one | C6/NS5 | QUIC handshake proof is bound to the connection, not replayable across connections |
+| 11.5.6 | **Spike DONE 2026-08-13 — see findings below.** Channel binding for QUIC | C6/NS5 | QUIC handshake proof is bound to the connection, not replayable across connections |
+
+#### 11.5.6 spike results (aioquic 1.3.0)
+
+**No RFC 5705 exporter.** `aioquic.tls.Context` has no `export_keying_material`, so the
+preferred anchor is unavailable.
+
+**Certificate access is asymmetric and partly private:**
+
+| Side | Path to the server certificate | API status |
+|---|---|---|
+| Server | `tls.certificate` | public attribute |
+| Client | `tls._peer_certificate` | **private** — set by `_set_peer_certificate()` |
+
+`QuicConnection` exposes no `tls`/`cert` attribute either, so the client's route is
+`protocol._quic.tls._peer_certificate` — two levels of private API.
+
+**The risk this creates.** Binding a security check to a private attribute means an
+aioquic upgrade can remove it silently. A channel binding that silently becomes
+unavailable is the worst failure mode: 11.5.21 already established that the handshake
+must *refuse* rather than degrade, so a rename would turn every QUIC connection into a
+hard failure — noisy, but only if the refusal path is right. If it were ever made
+tolerant, it would turn into a silent loss of MitM detection.
+
+**Options for the implementer, in order of preference:**
+
+1. **Certificate hash via the private attribute, guarded.** Pin `aioquic` in
+ `pyproject.toml`, and add a test that asserts `_peer_certificate` is reachable and
+ non-None on a live connection — so an upgrade fails CI rather than production. Keep
+ `make_proof()` refusing an empty binding.
+2. **Bind to `pk_node` instead of the channel.** For QUIC the MitM story differs from
+ WebRTC: signaling is not hub-relayed, and the client already learns `pk_node` from the
+ hub. The C3 mutual proof (node signs the transcript with `sk_node`) may be sufficient
+ connection authentication on its own — but note the QUIC client currently does **not**
+ verify the TLS certificate (`verify_mode` disabled, identity checked at the MNP layer),
+ so this option must be paired with pinning, or the TLS layer authenticates nobody.
+3. **Upstream an exporter.** Correct long-term answer, wrong timescale for 11.5.
+
+Recommendation: option 1 with the guard test, and open option 3 upstream.
### C — Mutual authentication