xenia-wire 0.2.0-alpha.3

PQC-sealed binary wire protocol for remote-control streams: ChaCha20-Poly1305 AEAD with epoch rotation, configurable sliding replay window (64-1024 slots), optional LZ4-before-seal, and signed consent ceremony with mandatory per-session fingerprint (HKDF-SHA-256). Pre-alpha — do not use in production.
Documentation
# Migration guide

Worked examples for every API break between published
`xenia-wire` versions. Paired with the per-release entries in
`CHANGELOG.md` — this file focuses on the concrete *before*
and *after* of integrator code, not the rationale for each
change.

Rust is the reference language; a concise TypeScript sketch
follows each Rust example to help alternate-language
implementers validate their own migration.

---

## 0.1.x → 0.2.0-alpha.1 (SPEC draft-03)

**Breaking at the signed-consent-body layer.** Envelope layout
(§1–§11) is unchanged; FRAME / INPUT / FRAME_LZ4 traffic is
still wire-compatible with any 0.1.x peer. Migration effort is
concentrated in four places:

1. Struct-literal constructions of the three signed `Core` types.
2. Pattern matches on `ConsentEvent`.
3. Call sites for `Session::observe_consent`.
4. Verify paths that previously called `ConsentRequest::verify`
   (etc.) directly.

You do NOT need to change anything at seal-path call sites that
don't touch consent, nor at `Session::new` / `Session::builder`
call sites.

### 1. Struct-literal `Core` constructions

The three signed bodies each gained a mandatory 32-byte
`session_fingerprint`. The canonical field order is normative
(SPEC §12.3) — do not reorder.

**Before (0.1.x):**

```rust
use xenia_wire::consent::{ConsentRequest, ConsentRequestCore, ConsentScope};

let core = ConsentRequestCore {
    request_id: 7,
    requester_pubkey: tech_sk.verifying_key().to_bytes(),
    valid_until: 1_700_000_300,
    scope: ConsentScope::ScreenAndInput,
    reason: "ticket #1234".into(),
    causal_binding: None,
};
let request = ConsentRequest::sign(core, &tech_sk);
```

**After (0.2.0-alpha.1) — recommended via the Session helper:**

```rust
use xenia_wire::consent::{ConsentRequestCore, ConsentScope};

let core = ConsentRequestCore {
    request_id: 7,
    requester_pubkey: tech_sk.verifying_key().to_bytes(),
    session_fingerprint: [0; 32], // placeholder, overwritten below
    valid_until: 1_700_000_300,
    scope: ConsentScope::ScreenAndInput,
    reason: "ticket #1234".into(),
    causal_binding: None,
};
let request = session
    .sign_consent_request(core, &tech_sk)
    .expect("session has a key installed");
```

The `Session::sign_consent_*` helpers derive the fingerprint
from the session's current key + source_id + epoch + the core's
`request_id`, overwrite the placeholder, and sign. Do NOT hand-
fill the placeholder with a meaningful value and then call the
raw `ConsentRequest::sign` — the receiver's fingerprint check
will fail unless your value matches the HKDF derivation bit-for-
bit.

**After — manual derivation (if you can't use the helper):**

```rust
let fp = session
    .session_fingerprint(7)
    .expect("session has a key");
let core = ConsentRequestCore {
    request_id: 7,
    requester_pubkey: tech_sk.verifying_key().to_bytes(),
    session_fingerprint: fp,
    valid_until: 1_700_000_300,
    scope: ConsentScope::ScreenAndInput,
    reason: "ticket #1234".into(),
    causal_binding: None,
};
let request = ConsentRequest::sign(core, &tech_sk);
```

The same pattern applies to `ConsentResponseCore` (→
`Session::sign_consent_response`) and `ConsentRevocationCore`
(→ `Session::sign_consent_revocation`).

**TypeScript sketch (for alternate-language implementers):**

```ts
// HKDF-SHA-256 per SPEC §12.3.1.
const info = new Uint8Array(17);
info.set(sourceId /* 8 bytes */, 0);
info[8] = epoch;
const be = new DataView(info.buffer);
be.setBigUint64(9, BigInt(request_id), /* littleEndian = */ false);

const fingerprint = await hkdfSha256({
    salt: new TextEncoder().encode("xenia-session-fingerprint-v1"),
    ikm: sessionKey, // 32 bytes
    info,
    length: 32,
});

const core = {
    request_id,
    requester_pubkey,
    session_fingerprint: fingerprint, // 32 bytes, field order matters
    valid_until,
    scope,
    reason,
    causal_binding: null,
};
const signature = await ed25519.sign(bincodeV1Encode(core), signingKey);
```

### 2. `ConsentEvent` variants carry `{ request_id }`

Every event is now a struct-shape carrying the `request_id` of
the message it describes. The state machine uses it to
distinguish legitimate ceremony progression from protocol
violations (SPEC §12.6.1).

**Before:**

```rust
use xenia_wire::consent::ConsentEvent;

session.observe_consent(ConsentEvent::Request);
session.observe_consent(ConsentEvent::ResponseApproved);
```

**After:**

```rust
use xenia_wire::consent::ConsentEvent;

session
    .observe_consent(ConsentEvent::Request { request_id: 7 })?;
session
    .observe_consent(ConsentEvent::ResponseApproved { request_id: 7 })?;
```

On the send side, you know `request_id` because you just picked
it. On the receive side, pull it from the deserialized core:
`ConsentEvent::Request { request_id: received_req.core.request_id }`.

### 3. `observe_consent` now returns `Result`

Legal transitions and benign no-ops return `Ok(state)`; protocol
violations (RevocationBeforeApproval, ContradictoryResponse,
StaleResponseForUnknownRequest) return
`Err(ConsentViolation)`. On violation the session state is NOT
mutated; the caller's contract is to tear down the session.

**Before:**

```rust
let state = session.observe_consent(ConsentEvent::Request);
```

**After:**

```rust
use xenia_wire::consent::ConsentViolation;

match session.observe_consent(ConsentEvent::Request { request_id: 7 }) {
    Ok(state) => { /* continue */ }
    Err(ConsentViolation::RevocationBeforeApproval { request_id }) => {
        // Hard fault — peer is broken or compromised. Tear down.
        return Err(MyAppError::PeerMisbehaved(request_id));
    }
    Err(ConsentViolation::ContradictoryResponse { request_id, prior_approved, new_approved }) => {
        // User tried to change their mind via a contradictory
        // Response; the correct primitive is Revocation. Log +
        // tear down; the peer's state machine is broken.
        return Err(MyAppError::ContradictoryConsent { request_id });
    }
    Err(ConsentViolation::StaleResponseForUnknownRequest { request_id }) => {
        return Err(MyAppError::StaleConsent(request_id));
    }
}
```

`ConsentViolation` implements `std::error::Error` via
`thiserror`, so `?`-propagation works if your error type
implements `From<ConsentViolation>` (or just map it).

### 4. Prefer `Session::verify_consent_*` over raw `.verify()`

`ConsentRequest::verify` (etc.) still exists and still checks
the Ed25519 signature. But it does NOT check the session
fingerprint — that's specific to the receiver's session state,
which the raw method doesn't have access to. The draft-03
verification contract (SPEC §12.3) requires BOTH checks.

**Before:**

```rust
if received_req.verify(Some(&expected_pubkey)) {
    // OK
}
```

**After:**

```rust
if session.verify_consent_request(&received_req, Some(&expected_pubkey)) {
    // OK — signature + fingerprint + pubkey all check out
}
```

The `Session::verify_consent_*` helpers transparently probe
both the current AND previous session keys for the fingerprint
compare, so a consent message signed just before rekey still
verifies during the grace window (added in 0.2.0-alpha.2; if
you're targeting alpha.1, the probe is current-key-only).

If you need only signature verification for an external audit
log — and you don't have access to the signing session — the
raw `.verify()` is still the right call. The draft-03
fingerprint is only checkable with the session key; an auditor
without it can still validate the pubkey-to-signature binding.

### 5. Test vector regeneration

If you pinned against vectors 07/08/09 from 0.1.x, their bytes
changed — the signed body's canonical encoding gained
`session_fingerprint`. Regenerate with:

```console
$ cargo run --example gen_test_vectors --all-features
```

The fingerprint value for vectors 07/08/09 under the fixture
key is `5b94fb75dd4d499825c7f26f32dea7dce067d59a5200584a2c9d7d9e18dfd7d4`
— same across all three (shared session + same `request_id=7`).
Alternate-language implementations that regenerate the
fingerprint locally under the fixture key MUST match that hex.

Vectors 10 / 11 / 12 are new event-sequence fixtures for the
three `ConsentViolation` variants. Format documented in
`test-vectors/10_revocation_before_approval.txt`.

---

## 0.2.0-alpha.1 → 0.2.0-alpha.2

No API break. Two receiver-side improvements:

1. `Session::verify_consent_*` now transparently probes the
   **previous** session key for the fingerprint compare. A
   consent message signed moments before a rekey (in flight
   during the grace window) now verifies correctly; previously
   it would false-reject. No code change required on integrator
   side — the behavior change is internal to the verify path.
2. SPEC §12.8 documents the timing-channel assumption on the
   verify pipeline (bincode deserialize + Ed25519 verify +
   constant-time fingerprint compare). Alternate-language
   implementers SHOULD audit their equivalents.
3. Event-sequence test vectors 10/11/12 ship for the three
   `ConsentViolation` variants.
4. A cargo-fuzz target `fuzz_observe_consent` is added to
   exercise the transition table under adversarial input.

No migration needed; just bump the dep.

---

## Older transitions (reference)

### 0.1.0-alpha.4 → 0.1.0-alpha.5 (SPEC draft-02r2)

Covered in full in `CHANGELOG.md`. Summary:

- `ConsentState::Pending` split into `LegacyBypass` (default
  for `Session::new`, sticky) and `AwaitingRequest` (opt-in via
  `SessionBuilder::require_consent(true)`). Exhaustive matches
  on `ConsentState` gain two new arms.
- New `SessionBuilder` at `Session::builder()`.
- Replay window now parameterized via
  `SessionBuilder::with_replay_window_bits(bits)` — valid
  values 64, 128, 256, 512, 1024. Default remains 64.

### 0.1.0-alpha.3 → 0.1.0-alpha.4

Pure reference-impl bug fix (per-key-epoch replay window,
issue #5). No API change. See `CHANGELOG.md` 0.1.0-alpha.4
entry for the bug details.