[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:
@@ -145,6 +145,46 @@ pub fn build_connect_request_body(
|
||||
tokens
|
||||
}
|
||||
|
||||
/// Build the NBFX token stream for `DisconnectIn`. Mirrors
|
||||
/// `AsbContracts.cs:109-114`:
|
||||
/// ```xml
|
||||
/// <DisconnectRequest xmlns="http://asb.contracts.messages/20111111">
|
||||
/// <ConsumerAuthenticationData>
|
||||
/// <Data>{encrypted-bytes}</Data>
|
||||
/// <InitializationVector>{iv-bytes}</InitializationVector>
|
||||
/// </ConsumerAuthenticationData>
|
||||
/// </DisconnectRequest>
|
||||
/// ```
|
||||
///
|
||||
/// One-way op (`IsOneWay = true` at `AsbContracts.cs:22`); typically
|
||||
/// signed with the connection validator (no `forceHmac`) right before
|
||||
/// closing the channel.
|
||||
pub fn build_disconnect_request_body(
|
||||
consumer_data: &[u8],
|
||||
initialization_vector: &[u8],
|
||||
) -> Vec<NbfxToken> {
|
||||
let mut tokens = vec![
|
||||
NbfxToken::Element {
|
||||
prefix: None,
|
||||
name: NbfxName::Inline("DisconnectRequest".to_string()),
|
||||
},
|
||||
NbfxToken::DefaultNamespace {
|
||||
value: NbfxText::Chars(MESSAGES_NS.to_string()),
|
||||
},
|
||||
NbfxToken::Element {
|
||||
prefix: None,
|
||||
name: NbfxName::Inline("ConsumerAuthenticationData".to_string()),
|
||||
},
|
||||
];
|
||||
tokens.extend(authentication_data_fields(
|
||||
consumer_data,
|
||||
initialization_vector,
|
||||
));
|
||||
tokens.push(NbfxToken::EndElement); // </ConsumerAuthenticationData>
|
||||
tokens.push(NbfxToken::EndElement); // </DisconnectRequest>
|
||||
tokens
|
||||
}
|
||||
|
||||
/// Build the NBFX token stream for `AuthenticateMeIn`. Sent
|
||||
/// **one-way** + **signed with `forceHmac=true`** per
|
||||
/// `MxAsbDataClient.cs:106-111`:
|
||||
@@ -974,6 +1014,43 @@ mod tests {
|
||||
assert!(found_pubkey_bytes, "ConsumerPublicKey/Data bytes not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnect_request_carries_data_and_iv_under_correct_wrapper() {
|
||||
let data = vec![0xDEu8, 0xAD];
|
||||
let iv = vec![0xBEu8, 0xEF];
|
||||
let body = build_disconnect_request_body(&data, &iv);
|
||||
assert!(matches!(
|
||||
&body[0],
|
||||
NbfxToken::Element { name: NbfxName::Inline(s), .. } if s == "DisconnectRequest"
|
||||
));
|
||||
// Walk for the ConsumerAuthenticationData wrapper.
|
||||
let mut saw_consumer_auth_data = false;
|
||||
for tok in &body {
|
||||
if let NbfxToken::Element {
|
||||
name: NbfxName::Inline(local),
|
||||
..
|
||||
} = tok
|
||||
{
|
||||
if local == "ConsumerAuthenticationData" {
|
||||
saw_consumer_auth_data = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(saw_consumer_auth_data);
|
||||
|
||||
let bytes_payloads: Vec<Vec<u8>> = body
|
||||
.iter()
|
||||
.filter_map(|tok| {
|
||||
if let NbfxToken::Text(NbfxText::Bytes(b)) = tok {
|
||||
Some(b.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(bytes_payloads, vec![data, iv]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_me_request_carries_data_and_iv() {
|
||||
let data = vec![0x01, 0x02, 0x03];
|
||||
|
||||
Reference in New Issue
Block a user