[M5] mxaccess-asb: F25 step 7 — Disconnect closes the session lifecycle

Mirrors `AsbContracts.cs:109-114` — same payload shape as
AuthenticateMe (Data + InitializationVector under
ConsumerAuthenticationData) but under the `<DisconnectRequest>`
wrapper. Sent one-way + signed (regular HMAC, no force) per
`AsbContracts.cs:22` (`IsOneWay = true`).

API additions:
* `build_disconnect_request_body(data, iv)` — NBFX token stream for
  the DisconnectRequest body.
* `AsbClient::disconnect()` — builds a fresh encrypted
  authentication-data blob via F23's `create_authentication_data()`
  (encrypts `local_pub || remote_pub` under the derived AES key
  with a fresh IV), wraps it in a DisconnectRequest, sends one-way
  signed.

2 new tests:
* `disconnect_request_carries_data_and_iv_under_correct_wrapper` —
  outer element name + Data/IV byte-payload order.
* `disconnect_writes_signed_one_way_envelope` — end-to-end via
  `tokio::io::duplex` peer; verifies the SizedEnvelope payload
  contains the `:disconnectIn` action string.

With Disconnect landed, AsbClient now covers the full session
lifecycle:
  send_preamble → connect → register_items / read / keep_alive
  / unregister_items → disconnect → send_end → stream shutdown

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 11:51:39 -04:00
parent 321b7963a4
commit 1b1ee1e0b7
4 changed files with 152 additions and 6 deletions
+5 -1
View File
@@ -46,7 +46,11 @@ move to `## Resolved` with a date + commit hash.
**Resolves when:** F19-F26 are all closed and the four DoD bullets above pass.
**Cumulative execution log.** F19 + F23 (`ed17c07`); F24 (`7611d9e`); F20 (`9dfd193`); F22 (`43c10a1`); F21 (`5f98558`); F25 step 1 (`25dbd8d`); F25 step 2 (`a2b8989`); F25 step 3 (`c4bf0a0`); F25 step 4 (`1e59249`); F25 step 5 (`9b8133f`); F25 step 6 landed in this commit:
**Cumulative execution log.** F19 + F23 (`ed17c07`); F24 (`7611d9e`); F20 (`9dfd193`); F22 (`43c10a1`); F21 (`5f98558`); F25 step 1 (`25dbd8d`); F25 step 2 (`a2b8989`); F25 step 3 (`c4bf0a0`); F25 step 4 (`1e59249`); F25 step 5 (`9b8133f`); F25 step 6 (`321b796`); F25 step 7 landed in this commit:
- F25 step 7: Disconnect operation (closes the connection lifecycle: Connect → ops → Disconnect → End → close). New `build_disconnect_request_body(data, iv)` mirrors `AsbContracts.cs:109-114` (`<DisconnectRequest><ConsumerAuthenticationData><Data/><InitializationVector/></ConsumerAuthenticationData></DisconnectRequest>`) — same payload shape as AuthenticateMe but under a different wrapper element. New `client::disconnect()` builds a fresh encrypted authentication-data blob via F23's `create_authentication_data` (encrypts `local_pub || remote_pub` under the derived AES key with a fresh IV), wraps it, and sends one-way + signed (regular HMAC, no force). 2 new tests: `disconnect_request_carries_data_and_iv_under_correct_wrapper` (checks wrapper element name + Data/IV byte ordering), and end-to-end `disconnect_writes_signed_one_way_envelope` via `tokio::io::duplex` peer that verifies the encoded SizedEnvelope contains the disconnectIn action string. With Disconnect landed, `AsbClient` now covers the full session lifecycle: `send_preamble().await? → connect().await? → register_items()/read()/keep_alive()/unregister_items() → disconnect().await? → send_end().await?`.
**Earlier slices:**
- F25 step 6 (commit `321b796`):
- F25 step 6: Connect + AuthenticateMe handshake — the critical-path piece that turns a fresh TCP stream into an authenticated session. New `build_connect_request_body` (carries connection-id GUID + consumer public key bytes; sent **unsigned** because no shared secret exists yet), `build_authenticate_me_request_body` (carries encrypted Data + IV; sent **one-way + signed with `forceHmac=true`** per `MxAsbDataClient.cs:106-111`), `decode_connect_response` (extracts ServicePublicKey, optional ServiceAuthenticationData, optional ConnectionLifetime — handles the `:V2` Apollo lifetime suffix that toggles F23's encryption mode), `AuthenticationDataBytes` struct, and `client::connect` orchestration that runs the full handshake: ConnectRequest → ConnectResponse → `accept_connect_response` (derives shared secret) → `create_authentication_data` (encrypted local_pub || remote_pub) → AuthenticateMeRequest one-way. 6 new tests cover ConnectRequest body shape (carries hyphenated GUID + public-key bytes), AuthenticateMe body shape (Data + IV bytes), ConnectResponse round-trip with all optional fields, ConnectResponse without optional fields, MissingField error when ServicePublicKey absent, and an end-to-end client::connect handshake test via `tokio::io::duplex` peer that synthesises a ConnectResponse with bob's public key (so DH shared-secret derivation works) and drains the AuthenticateMe one-way SizedEnvelope. **Wire-byte caveat**: WCF XML serialization of `<PublicKey><Data>byte[]</Data>` may include `xsi:type` attributes or distinct namespaces that this builder doesn't yet emit; live-probe iteration will reconcile.
**Earlier slices:**