|
| 1 | +# Round-3 review — delta since draft-02r1 |
| 2 | + |
| 3 | +**Target version**: `xenia-wire 0.2.0-alpha.2` / SPEC **draft-03**. |
| 4 | +**Assumed prior context**: you last reviewed **draft-02r1** |
| 5 | +(`xenia-wire 0.1.0-alpha.3` / `alpha.4`, April 2026), which flagged |
| 6 | +four open design items: session-binding (loose), split-Pending, |
| 7 | +duplicate/conflict consent semantics, configurable replay window. |
| 8 | + |
| 9 | +This document is the scoped follow-up. It is deliberately short: |
| 10 | + |
| 11 | +- **§1** is the deltas since draft-02r1 — nothing you haven't seen |
| 12 | + before if you tracked Appendix B, but collected here for quick |
| 13 | + orientation. |
| 14 | +- **§2** is six targeted questions ranked by cryptographic weight. |
| 15 | + Questions 1-3 are where we genuinely would like an independent |
| 16 | + opinion; 4-6 are lower-risk "any concerns?" items. |
| 17 | + |
| 18 | +Section-number references (e.g., §12.3.1) are to SPEC.md at |
| 19 | +`v0.2.0-alpha.2`. Source-file references are to the reference |
| 20 | +implementation at the same tag. |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## 1. Deltas since draft-02r1 |
| 25 | + |
| 26 | +### draft-02r2 (shipped `0.1.0-alpha.5`, no wire change) |
| 27 | + |
| 28 | +- Split `ConsentState::Pending` into **`LegacyBypass`** (default, |
| 29 | + sticky) and **`AwaitingRequest`** (opt-in via |
| 30 | + `SessionBuilder::require_consent(true)`). Pure receiver-local; |
| 31 | + no envelope bytes change. |
| 32 | +- Replay window parameterized: `W ∈ {64, 128, 256, 512, 1024}` |
| 33 | + bits, default 64. Multi-word bitmap shift internal to |
| 34 | + `ReplayWindow`. Peers agree on `W` out-of-band. |
| 35 | + |
| 36 | +These are bookkeeping; we don't expect review input unless you see |
| 37 | +a hazard. |
| 38 | + |
| 39 | +### draft-03 (shipped `0.2.0-alpha.1`, **breaking** at signed-body layer) |
| 40 | + |
| 41 | +- **Mandatory `session_fingerprint: [u8; 32]`** on all three signed |
| 42 | + cores (`ConsentRequestCore`, `ConsentResponseCore`, |
| 43 | + `ConsentRevocationCore`). Canonical field order pinned in §12.3 |
| 44 | + and is normative. Derivation specified in §12.3.1: |
| 45 | + |
| 46 | + ``` |
| 47 | + salt = b"xenia-session-fingerprint-v1" (28 bytes ASCII) |
| 48 | + ikm = current AEAD session_key (32 bytes) |
| 49 | + info = source_id || epoch || request_id_be (8 + 1 + 8 = 17 bytes) |
| 50 | + L = 32 |
| 51 | + HKDF-SHA-256(salt, ikm).expand(info, L) -> fingerprint |
| 52 | + ``` |
| 53 | + |
| 54 | +- **Normative transition table for the consent state machine** |
| 55 | + (§12.6.1). Covers every (state, event, `request_id`-predicate) |
| 56 | + tuple. Three defined protocol violations surface as |
| 57 | + `WireError::ConsentProtocolViolation(ConsentViolation)`: |
| 58 | + |
| 59 | + - `RevocationBeforeApproval { request_id }` |
| 60 | + - `ContradictoryResponse { request_id, prior_approved, new_approved }` |
| 61 | + - `StaleResponseForUnknownRequest { request_id }` |
| 62 | + |
| 63 | + `ConsentEvent` variants now carry `{ request_id }`. |
| 64 | + `Session::observe_consent` returns `Result<ConsentState, |
| 65 | + ConsentViolation>`. On violation, state is NOT mutated; the wire |
| 66 | + does not tear down the transport. |
| 67 | + |
| 68 | +- **UI-guidance subsection §12.6.2** — "change of mind after |
| 69 | + approval" MUST be expressed as a fresh `ConsentRevocation`, never |
| 70 | + as a contradictory `ConsentResponse`. This was the contentious |
| 71 | + decision during internal review: we considered "later-wins" on |
| 72 | + contradictory `ConsentResponse`, rejected it because (a) the |
| 73 | + `ConsentResponseCore` body carries no timestamp, and (b) a |
| 74 | + captured late-Denied from a prior session could force teardown |
| 75 | + on a new Approved one if the fingerprint ever reused across |
| 76 | + ceremonies. Rejection aligns with the decision to make |
| 77 | + fingerprint mandatory. |
| 78 | + |
| 79 | +- **Security-properties rewrite §12.8**: the "LOOSE binding" |
| 80 | + bullet from draft-02r1 becomes "TIGHT binding". New |
| 81 | + protocol-violation-detection bullet for the transition table. |
| 82 | + |
| 83 | +### draft-03 hardening (shipped `0.2.0-alpha.2`, no wire or API change) |
| 84 | + |
| 85 | +- **Rekey-aware fingerprint verify.** |
| 86 | + `Session::verify_consent_{request,response,revocation}` now |
| 87 | + probe **both** the current and (if present) the previous session |
| 88 | + keys when comparing the embedded fingerprint. A consent message |
| 89 | + AEAD-verified under the previous key during the grace window |
| 90 | + now also passes the fingerprint check. §12.3.1 rekey |
| 91 | + interaction spells this out. |
| 92 | +- **Timing-channel assumption (§12.8)** — new paragraph. Asserts |
| 93 | + the verify pipeline (bincode deserialize, Ed25519 verify, |
| 94 | + fingerprint compare) MUST NOT branch on secret-dependent bytes. |
| 95 | + Reference impl ships an inline constant-time 32-byte compare |
| 96 | + (`src/session.rs::ct_eq_32`); alternate-language implementers |
| 97 | + are on the hook for auditing bincode-equivalents and Ed25519 |
| 98 | + libs. |
| 99 | +- **Test vectors 10/11/12** for the three `ConsentViolation` |
| 100 | + variants (event-sequence format; grammar in |
| 101 | + `test-vectors/10_revocation_before_approval.txt`). |
| 102 | +- **`cargo-fuzz` target `fuzz_observe_consent`** asserting four |
| 103 | + invariants per step: no panic, state always valid, seal-gate |
| 104 | + matches state, violations never mutate state. |
| 105 | + |
| 106 | +Not in this review cycle: the paper draft (no crypto changes), the |
| 107 | +reference LZ4 path (unchanged since draft-02), the replay window |
| 108 | +parameterization (mechanical generalization of the draft-02 fixed |
| 109 | +64). |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +## 2. Questions ranked by weight |
| 114 | + |
| 115 | +### 2.1 `session_fingerprint` `info` composition (§12.3.1) |
| 116 | + |
| 117 | +**The question.** Is `info = source_id || epoch || request_id_be` |
| 118 | +the right input set? |
| 119 | + |
| 120 | +**Alternatives considered:** |
| 121 | + |
| 122 | +| Variant | Property | |
| 123 | +|---|---| |
| 124 | +| `info = []` | Session-key-only binding. Per-ceremony replay opens up inside a single session. | |
| 125 | +| `source_id ‖ epoch` | Session-identity binding. A `ConsentResponse` signed for `request_id=7` is replayable as one for `request_id=8` — undesirable. | |
| 126 | +| **`source_id ‖ epoch ‖ request_id_be` (chosen)** | Per-ceremony binding. What's shipped. | |
| 127 | +| `source_id ‖ epoch ‖ request_id_be ‖ pld_type` | Per-message-type per-ceremony binding. Would give a distinct fingerprint per message kind. Avoids a hypothetical attack where a captured Response is replayed as a Revocation; we don't currently see that attack, but it's not impossible. | |
| 128 | + |
| 129 | +**Specific ask.** Is the chosen set sufficient for a signed-consent |
| 130 | +replay threat model, or should `pld_type` be mixed into `info`? |
| 131 | + |
| 132 | +### 2.2 Big-endian `request_id` in `info` |
| 133 | + |
| 134 | +**The question.** SPEC §12.3.1 specifies `request_id_be` — the u64 |
| 135 | +in big-endian. Every other integer on the Xenia wire is |
| 136 | +little-endian (per bincode v1 default; nonce sequence bytes are |
| 137 | +explicitly LE per §3). The mixed convention is intentional — we |
| 138 | +wanted the `info` byte-order to differ from the ambient wire so |
| 139 | +that an implementation reaching for a "byte-encode this u64 the |
| 140 | +usual way" path would produce the wrong fingerprint and fail |
| 141 | +loudly rather than silently. |
| 142 | + |
| 143 | +**Specific ask.** Is this mistake-reduction smart, or is it a |
| 144 | +footgun that buys nothing because the endianness is already |
| 145 | +specified normatively either way? |
| 146 | + |
| 147 | +### 2.3 Timing side-channel in `verify_fingerprint_either_epoch` |
| 148 | + |
| 149 | +**The question.** On the receive path, `verify_consent_*` derives |
| 150 | +the fingerprint against the **current** key, compares, and — only |
| 151 | +if that fails — derives again against the **previous** key. |
| 152 | +That's one HKDF call on match, two on mismatch. Relevant source: |
| 153 | +`src/session.rs::verify_fingerprint_either_epoch` (around line |
| 154 | +340-360 in the 0.2.0-alpha.2 tree). |
| 155 | + |
| 156 | +An attacker observing verify-path timing could distinguish |
| 157 | +"accepted under current key" from "accepted under prev key" from |
| 158 | +"rejected". The latency delta is ~one HKDF-SHA-256 derivation |
| 159 | +(~a few microseconds on commodity hardware). Is this exploitable? |
| 160 | + |
| 161 | +**Mitigation considered.** Always derive both fingerprints |
| 162 | +regardless of first-match outcome; compare each; OR the |
| 163 | +constant-time compare results. Constant-time verify path at the |
| 164 | +cost of one extra HKDF call per verify. Open to your opinion on |
| 165 | +whether this is worth the complexity. |
| 166 | + |
| 167 | +### 2.4 Transition-table completeness (§12.6.1) |
| 168 | + |
| 169 | +**The question.** The table enumerates (state × event × |
| 170 | +`request_id`-predicate). One corner case I want to probe: |
| 171 | +`LegacyBypass` + any event → sticky no-op, including a perfectly |
| 172 | +valid `ConsentRequest` whose fingerprint matches a local |
| 173 | +derivation. |
| 174 | + |
| 175 | +Is sticky the right rule, or should there be a documented |
| 176 | +"upgrade from LegacyBypass to ceremony mode" path for deployments |
| 177 | +that start permissive and later want to enforce? Currently the |
| 178 | +only upgrade path is constructing a new `Session` via |
| 179 | +`SessionBuilder::require_consent(true)`. |
| 180 | + |
| 181 | +### 2.5 `ConsentResponse` / `ConsentRevocation` timestamp asymmetry |
| 182 | + |
| 183 | +**The question.** `ConsentRevocationCore` has `issued_at: u64` |
| 184 | +(Unix epoch seconds). `ConsentResponseCore` does NOT. This was |
| 185 | +intentional in draft-02 and carried forward; we want to confirm |
| 186 | +it's still OK given the draft-03 `session_fingerprint`. |
| 187 | + |
| 188 | +Argument for leaving the asymmetry: within a single ceremony, the |
| 189 | +state machine forbids a replayed Response (idempotent same-value |
| 190 | +Response is a no-op; differing-value is `ContradictoryResponse` |
| 191 | +hard-error). Across ceremonies, the fingerprint's `request_id_be` |
| 192 | +distinguishes them. So no replay window exists. |
| 193 | + |
| 194 | +Argument for adding `issued_at` to ConsentResponse anyway: the |
| 195 | +audit-trail use case (§12.8 third-party-verifiable signed consent) |
| 196 | +benefits from knowing *when* the user signed the approval, not |
| 197 | +just *that* they did. Currently a replayed (same-value) |
| 198 | +`ConsentResponse` would be indistinguishable from the original in |
| 199 | +a frozen transcript. |
| 200 | + |
| 201 | +### 2.6 General coherence of §12.8 security properties + §12.10 out-of-scope |
| 202 | + |
| 203 | +**The question.** §12.8 now enumerates: third-party-verifiable |
| 204 | +signed consent, TIGHT session binding, protocol-violation |
| 205 | +detection, no human-identity binding, and the timing-channel |
| 206 | +assumption. §12.10 lists the explicit out-of-scope items (coerced |
| 207 | +consent, human-identity fraud, clock-skew attacks). |
| 208 | + |
| 209 | +Is there a property draft-03 implicitly offers that §12.8 should |
| 210 | +claim explicitly? Or a threat draft-03 implicitly tolerates that |
| 211 | +§12.10 should flag? Open-ended reality-check question. |
| 212 | + |
| 213 | +--- |
| 214 | + |
| 215 | +## 3. Pointers |
| 216 | + |
| 217 | +- **SPEC**: `SPEC.md` at tag `v0.2.0-alpha.2` on |
| 218 | + `Luminous-Dynamics/xenia-wire`. §12.3, §12.3.1, §12.6.1, §12.6.2, |
| 219 | + §12.8 are the draft-03 deltas. Appendix B row for draft-03 |
| 220 | + records the changelog. |
| 221 | +- **Reference implementation**: `src/consent.rs` (all three Core |
| 222 | + types + ConsentViolation + ConsentEvent), `src/session.rs` |
| 223 | + (`session_fingerprint`, `session_fingerprint_from_key`, |
| 224 | + `verify_fingerprint_either_epoch`, `observe_consent` with the |
| 225 | + draft-03 transition table, `ct_eq_32`), `src/error.rs` |
| 226 | + (`WireError::ConsentProtocolViolation`). |
| 227 | +- **Test vectors**: `test-vectors/07_consent_request.*`, |
| 228 | + `08_consent_response.*`, `09_consent_revocation.*` for the |
| 229 | + happy-path signed bodies; `10_revocation_before_approval.txt`, |
| 230 | + `11_contradictory_response.txt`, `12_stale_response.txt` for the |
| 231 | + violation event-sequence DSL (grammar documented in file 10). |
| 232 | +- **Fuzz target**: `fuzz/fuzz_targets/fuzz_observe_consent.rs` |
| 233 | + with the four invariants. |
| 234 | +- **Migration**: `MIGRATION.md` (0.1.x → 0.2.0-alpha.1, plus |
| 235 | + alpha.1 → alpha.2). Paired with `CHANGELOG.md` per-release |
| 236 | + entries. |
| 237 | + |
| 238 | +## 4. Feedback logistics |
| 239 | + |
| 240 | +Any form works. If you prefer structured feedback: a per-section |
| 241 | +reply keyed to the six questions above is the highest-bandwidth |
| 242 | +format. If you'd rather leave comments inline in a PR against the |
| 243 | +`SPEC.md` tree, that's also fine — we'll open a draft `spec-review` |
| 244 | +PR on request. |
| 245 | + |
| 246 | +Thank you for taking the time. Rounds 1 and 2 caught real issues; |
| 247 | +this round is targeted at confirming the close-out rather than a |
| 248 | +full re-read. |
0 commit comments