f14580e0db585ff5b5c2def340d84ff368563288
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f14580e0db |
[M5] mxaccess-asb: F28 canonical-XML signing wired + registry-driven DH params
Adds `xml_canonical` module that emits XmlSerializer-compatible canonical XML for the five primary `ConnectedRequest` shapes (AuthenticateMe, Disconnect, KeepAlive, RegisterItemsRequest, UnregisterItemsRequest). Six fixture-comparison tests verify byte-exact match against captured .NET output, including the empty-MAC-IV variant that the live signing flow uses (`authenticate-me-empty-mac-iv.xml`, 896 bytes; new `emit_data_ns_byte_array` helper picks self-closing form for empty byte[]). Plumbing: `AsbAuthenticator::peek_next_message_number` exposes the pre-allocated message number; `AsbClient::send_signed_envelope[_one_way]` gain an `xml_for_signing: Option<&[u8]>` parameter. `connect`, `disconnect`, `keep_alive`, `register_items`, `unregister_items` now build a pre-signing `ConnectionValidator` (empty MAC + IV) + emit the canonical XML + pass the bytes through to HMAC. Other ops (Read, Write, Subscription) keep the legacy NBFX-bytes path until F28 expands to cover their request shapes. Live-bring-up wiring: - `tools/Get-AsbPassphrase.ps1` now exports `MX_ASB_DH_PRIME`, `MX_ASB_DH_GENERATOR`, `MX_ASB_DH_HASH_ALGORITHM` (always — even when empty, so the example can distinguish "no env var" from "registry says empty"), and `MX_ASB_DH_KEY_SIZE`. - `examples/asb-subscribe.rs` honours those env vars to override `CryptoParameters::defaults()`. Each AVEVA install picks its own DH group at provisioning time (768-bit prime is typical, vs the .NET reference's 1024-bit fallback that we previously hardcoded). Empty hashAlgorithm in the registry maps to `HashAlgorithm::Unrecognised`, matching `AsbSystemAuthenticator.CreateHmac:84-93` semantics where empty + forceHmac=true → HMAC-SHA1. - `MxAsbClient.Probe --dump-signed-xml` flag (added in earlier commit) now traces the live HMAC inputs (`asb.sign.xml-utf8-len`, `asb.sign.xml-b64`, `asb.sign.hmac-b64`, etc.) so the Rust port can diff its canonical XML against .NET's byte-for-byte for any live scenario (env-driven via `Action<string>? sharedTrace`). Wire-format alignment for `XmlSerializer` parity: - `ItemIdentity::default()` and `absolute_by_name` now use `Some(String::new())` for null-able strings (matches .NET's `CreateAbsoluteItem` setting `ContextName = string.Empty` not null). - `read_unicode_string` returns `Some(String::new())` for length-0 rather than `None` — mirrors .NET's `AsbBinary.ReadUnicodeString: return string.Empty for byteLength == 0`. Wire format genuinely cannot distinguish null from empty (both encode as 4 bytes of zero); callers that need to preserve the distinction MUST track it in their domain types before encoding. Live status (post-fix): Connect handshake completes end-to-end. The canonical XML our emitter produces matches .NET's structure byte-for- byte (verified by fixture comparison). DH prime/generator/hash now match the live registry values. Despite all this, AuthenticateMe still produces a generic dispatcher fault on the server — there's at least one more subtle wire-byte or crypto mismatch that needs isolation. F28 stays open with that note. Workspace: 709 unit tests pass (was 702 + 7 new xml_canonical tests). Clippy: clean (`-D warnings`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d1e887b91b |
[M5] mxaccess-asb-nettcp/asb: Connect handshake live + SOAP fault detection
Live-bring-up reconciliation against AVEVA's MxDataProvider on Windows.
Connect now completes end-to-end (real DH key exchange, apollo:V2
encryption, ServicePublicKey/ServiceAuthenticationData populated). Five
fixes land:
1. NBFX `PrefixElement_a..z` (0x5E-0x77) and `PrefixAttribute_a..z`
(0x26-0x3F) decode + encode arms. The server's ConnectResponse hit
`0x65 = PrefixElement_h` for a dynamically-named element and our
decoder bailed with `unknown NBFX record byte 0x65`. Both directions
now round-trip; encoder picks short-form when prefix is a single
lowercase ASCII letter.
2. xmlns redeclaration on `<Data>` AND `<InitializationVector>` inside
`AuthenticationData` / `PublicKey`. `[XmlType(Namespace = ...)]` on
AuthenticationData / PublicKey (`AsbContracts.cs:350-381`) means
XmlSerializer emits `xmlns="..."` on each direct child. The default-
ns scope ends at `</Data>`, so `<InitializationVector>` needs its own
redeclaration to stay in the data namespace; without it the server
fell back to messages-namespace and the deserialiser threw an
`InternalServiceFault`.
3. SOAP-fault detection in `AsbClient::send_envelope`. New
`ClientError::SoapFault { action, code, reason }` surfaces when the
response Action header matches the canonical `dispatcher/fault`
template; previously body decoders blindly ran and surfaced
`MissingField { field: "Status" }` masking the actual fault. Reason
text is extracted as the longest `NbfxText::Chars` in the body —
robust against the `nbfs.rs` static-dictionary id mismatches.
4. Identified blocker (filed as F28): signed-request HMAC currently
covers the NBFX wire bytes, but .NET's `AsbSystemAuthenticator.Sign`
HMACs `Encoding.UTF8.GetBytes(request.ToXml())` — the canonical XML
serialisation via `XmlSerializer` with namespace
`urn:invensys.schemas` (`AsbSerialization.cs:12-48`). Until the Rust
port emits identical XML bytes for `ConnectedRequest` subclasses,
AuthenticateMe / RegisterItems / every signed RPC fault on the
server. Connect itself is unsigned (`ServiceMessage` not
`ConnectedRequest`) which is why it works today.
5. Identified `nbfs.rs` static-dictionary id drift (filed as F29): wire
uses Fault=134/Code=142/Reason=144/Text=146/Value=154/Subcode=156
but our table has them at 114/122/124/126/134/136. Off by 20 from
id 114+ — 10 missing entries between `s` (id 112) and `Fault`. No
request-side impact (we only encode IDs ≤44, all correct); the SOAP
fault decode walks text records directly so it sidesteps the issue.
Workspace: 702 tests pass (no test count delta — wire-only fixes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2867310817 |
[M5] mxaccess-asb: WCF binary message header (action+to dict pre-pop)
Adds the binary header block that WCF prepends to SizedEnvelope
payloads. Reverse-engineered from .NET probe wire bytes captured via
asb-relay.
Wire form (per the .NET capture analysis in the previous commit):
```
[outer length, multibyte-int31]
[string-1 length, multibyte-int31] [UTF-8 bytes] ← dict id 1 (action)
[string-2 length, multibyte-int31] [UTF-8 bytes] ← dict id 3 (to)
[NBFX <s:Envelope>...]
```
Inside the NBFX envelope, `<a:Action>` and `<a:To>` reference the
pre-pop strings via `DictionaryText 0xAA {odd-id}` instead of inlining
their values. The header strings get assigned odd dict ids
(1, 3, 5, ...); even ids stay reserved for the [MC-NBFS] static dict.
Encode side:
* `encode_envelope` now emits header [action, to] before NBFX. `to_uri`
defaults to empty string when None — caller-supplied `with_to(uri)`
is the supported path.
* AsbClient's `send_envelope` and `send_envelope_one_way` auto-fill
`to_uri` from `self.via_uri` when not set.
* New private `encode_binary_header(strings)` helper.
Decode side:
* New `parse_binary_header_prefix(input)` heuristically detects + parses
the header (look for plausible NBFX element record byte 0x40-0x77 at
the offset implied by the outer length).
* New `resolve_with_header(text, dynamic, header)` resolves
`DictionaryText` with odd id by indexing into header.strings; even
ids fall through to static-dict lookup as before.
Tests pass (72) — round-trip envelope → bytes → envelope recovers
action through the new dict-id resolution path.
Live status: this commit gets us further but the connect SOAP
envelope still TCP-RSTs at SMSvcHost. The remaining delta vs the .NET
capture is structural NBFX optimisation: .NET uses single-letter
prefix-element/attribute records (0x44-0x77 PrefixDictionaryElement
_<a-z>, 0x0C-0x25 PrefixDictionaryAttribute_<a-z>, 0x0B
DictionaryXmlnsAttribute) while our F21 encoder always uses the long
forms (0x43 prefix-string + name-dict-id, etc.). Logically
equivalent but WCF's parser likely strict on which form it accepts.
Next iteration will add short-form encoding to F21 for single-letter
prefixes (s:, a:, h:, i:) which covers every namespace prefix in our
envelope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9876b4ebb4 |
[M5] mxaccess-asb: F25 step 10 — PublishWriteComplete + DeleteMonitoredItems
Closes out the F25 operation matrix. AsbClient now wraps every
IASBIDataV2 operation:
Lifecycle: connect / disconnect / send_end / send_preamble / keep_alive
Items: register_items / unregister_items / read / write
Subscriptions:create_subscription / add_monitored_items / publish
/ delete_monitored_items / delete_subscription
Write cb: publish_write_complete
API additions:
* `build_publish_write_complete_request_body()` — empty wrapper
per `AsbContracts.cs:204-205`. No body fields beyond inherited
ConnectionValidator.
* `decode_publish_write_complete_response` — returns count of
`<ItemWriteComplete>` elements observed. Per-element decode
(Status + WriteHandle) deferred to a later iteration since
ItemWriteComplete is regular WCF DataContract rather than the
binary fast-path.
* `build_delete_monitored_items_request_body` — same MonitoredItem
shape as AddMonitoredItems but omits RequireId per `cs:268-277`.
* `decode_delete_monitored_items_response` — per-item Status array.
* Client wrappers: `publish_write_complete()`,
`delete_monitored_items(subscription_id, items)`.
6 new tests:
* `publish_write_complete_body_is_empty_wrapper` — body shape.
* `publish_write_complete_response_counts_item_write_complete_elements`
— counts 2 / 0 elements correctly.
* `publish_write_complete_response_zero_when_no_callbacks`.
* `delete_monitored_items_body_carries_subscription_id_and_items`.
* `delete_monitored_items_body_omits_require_id_field`.
* `delete_monitored_items_response_round_trip`.
Workspace: 701 tests pass (was 695, +6).
Stubbed for future iterations:
* ItemWriteComplete per-element decode (Status + WriteHandle) once
a live capture confirms the WCF DataContract XML wire form.
* Optional MonitoredItem fields (Active / TimeDeadband /
ValueDeadband / UserData) — same wire-byte uncertainty.
* Optional WriteValue fields (Comment / Timestamp / etc.).
All wire-byte caveats trace back to live-probe reconciliation
against an actual AVEVA VM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0441a2e693 |
[M5] mxaccess-asb: F25 step 9 — Write operation
Closes the highest-value remaining IASBIDataV2 op. With Write landed,
the read+write+subscribe path is functionally complete in-memory.
API additions:
* `MinimalWriteValue { value: AsbVariant }` — carries just the Value
payload. Optional ArrayElementIndex / Comment / HasQT / Status /
Timestamp fields are deferred to a later iteration once a live
capture confirms the WCF DataContract XML form.
* `build_write_request_body(items, values, write_handle)` per
`AsbContracts.cs:181-194`:
```xml
<WriteBasicRequest xmlns="urn:msg.data.asb.iom:2">
<Items><ASBIData>{ItemIdentity[] binary}</ASBIData></Items>
<Values>
<WriteValue><Value><ASBIData>{Variant binary}</ASBIData></Value></WriteValue>
...
</Values>
<WriteHandle>{i32}</WriteHandle>
</WriteBasicRequest>
```
Items array uses the IAsbCustomSerializableType binary fast-path;
each Value's inner Variant also uses the fast-path. WriteHandle is
an Int32 (opaque correlation echoed in PublishWriteComplete).
* `decode_write_response` — per-item Status array (mirrors the
unregister/register pattern).
* `AsbClient::write(items, values, write_handle)` — thin wrapper.
4 new tests:
* `write_request_body_carries_items_values_and_write_handle` — body
shape sanity (WriteHandle = 7 Int32, WriteValue element present).
* `write_request_body_pairs_items_and_values_arrays` — 2 items + 2
values produces 2 WriteValue elements.
* `write_response_round_trips_status_array` — Status decode.
* `write_response_missing_status_fails` — graceful MissingField
error.
Workspace: 695 tests pass (was 691, +4).
Stubbed for next F25 iterations:
* `PublishWriteComplete` — empty request, `ItemWriteComplete[]`
response.
* `DeleteMonitoredItems` — mirrors AddMonitoredItems pattern.
* Optional WriteValue fields (Comment / Timestamp / etc.) once a
live capture confirms the wire-byte layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b543eb1f84 |
[M5] mxaccess-asb: F25 step 8 — subscription operations
CreateSubscription / AddMonitoredItems / Publish / DeleteSubscription. Completes the IASBIDataV2 read-and-subscribe path; remaining ops (Write/PublishWriteComplete/DeleteMonitoredItems) are mechanical extensions of the same pattern. Contracts: * `MonitoredItemValue` codec (IAsbCustomSerializableType binary fast-path: ItemIdentity + RuntimeValue + AsbVariant per `AsbContracts.cs:1064-1068`) with array codec (4-byte int32 count + per-element body, mirrors `WriteArrayToStream` at `cs:1095-1103`). Request builders: * `build_create_subscription_request_body(max_queue_size, sample_interval)` — primitive fields per `cs:215-223`. * `build_delete_subscription_request_body(subscription_id)` — primitive field per `cs:232-237`. * `build_publish_request_body(subscription_id)` — primitive field per `cs:287-292`. * `build_add_monitored_items_request_body(subscription_id, items, require_id)` — minimal MonitoredItem shape (Item + SampleInterval + Buffered). Full optional-field set (Active/TimeDeadband/ValueDeadband/UserData) deferred to a later iteration once a live capture confirms the WCF DataContract XML wire form. Response decoders: * `decode_create_subscription_response` — single int64 SubscriptionId field. Decoder accepts Int64Text, Int32Text, Zero/One, or numeric-string Chars (covers all WCF binary numeric encodings). * `decode_add_monitored_items_response` — Status array + ItemCapabilities-presence flag (mirrors RegisterItemsResponse). * `decode_publish_response` — Status array + Values (MonitoredItemValue) array. `BodyField::Int64Element` variant added for the primitive SubscriptionId / MaxQueueSize / SampleInterval fields. `uint64` helper casts to i64 (covers proven value range; if ulong > i64::MAX ever appears we'll add UInt64Text to F21's NbfxText enum). Client wrappers (4 new methods on AsbClient): * `create_subscription(max_queue_size, sample_interval)` * `add_monitored_items(subscription_id, items, require_id)` * `publish(subscription_id)` * `delete_subscription(subscription_id)` 11 new tests cover: * MonitoredItemValue round-trip + array round-trip. * CreateSubscription request body shape (Int64 payloads). * CreateSubscription response decoder via Int64Text. * CreateSubscription response decoder via Chars text fallback. * CreateSubscription response missing-field error. * AddMonitoredItems body carries SubscriptionId + MonitoredItem elements. * AddMonitoredItems response Status round-trip. * DeleteSubscription body carries SubscriptionId. * Publish request body shape. * Publish response Status + Values round-trip. Workspace: 691 tests pass (was 680, +11). The asb-subscribe example can now do create_subscription → add_monitored_items → publish-loop → delete_subscription once wire-byte reconciliation against a live capture confirms the MonitoredItem XML shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1b1ee1e0b7 |
[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> |
||
|
|
321b7963a4 |
[M5] mxaccess-asb: F25 step 6 — Connect/AuthenticateMe handshake
Critical-path piece that turns a fresh TCP stream into an
authenticated session. With this slice landed, an `AsbClient` can
now do `send_preamble().await? -> connect().await? -> register_items()`
end-to-end against a peer.
Operations API additions:
* `build_connect_request_body(connection_id, public_key)` — first op
on a fresh session. **Unsigned** (no ConnectionValidator header)
because the authenticator hasn't received the service key yet.
Wire shape: `<ConnectRequest xmlns="…messages/20111111">
<ConnectionId>{guid-text}</ConnectionId>
<ConsumerPublicKey><Data>{pubkey-bytes}</Data></ConsumerPublicKey>
</ConnectRequest>` per `AsbContracts.cs:78-86`.
* `build_authenticate_me_request_body(data, iv)` — second op,
**one-way + signed with `forceHmac=true`** per `MxAsbDataClient.cs
:106-111`. Carries the encrypted `local_pub || remote_pub` blob
produced by F23's `create_authentication_data()`.
* `ConnectResponse { service_public_key, service_authentication_data,
connection_lifetime }` + `AuthenticationDataBytes { data, iv }`.
* `decode_connect_response(body, dict)` — extracts ServicePublicKey
(required), optional ServiceAuthenticationData, optional
ConnectionLifetime. The lifetime's `:V2` suffix is what F23
inspects to toggle Apollo (raw AES) vs Baktun (deflate-then-AES)
encryption.
Client API addition:
* `AsbClient::connect()` — orchestrates the full handshake:
1. Build + send ConnectRequest (unsigned) carrying our DH public
key + connection-id GUID.
2. Decode ConnectResponse.
3. `authenticator.accept_connect_response(...)` — feeds the
service public key + lifetime into F23 so it derives the
shared secret and picks Apollo/Baktun.
4. `authenticator.create_authentication_data()` — encrypts
`local_pub || remote_pub` under the derived AES key.
5. Send AuthenticateMeRequest (one-way, signed with HMAC-SHA1
forced).
Returns the `ConnectResponse` so callers can inspect the
negotiated connection lifetime.
6 new tests:
* ConnectRequest carries hyphenated GUID + raw public-key bytes.
* AuthenticateMe carries Data + IV bytes in order.
* ConnectResponse round-trip with all optional fields populated.
* ConnectResponse round-trip without optional fields.
* ConnectResponse decoder surfaces MissingField when
ServicePublicKey is absent.
* End-to-end client::connect handshake via `tokio::io::duplex`
peer that synthesises a ConnectResponse using bob's public key
(so DH shared-secret derivation actually works) and drains the
AuthenticateMe one-way SizedEnvelope.
Wire-byte caveat documented inline: WCF XML serialization may add
`xsi:type` attributes / distinct namespaces around <PublicKey> /
<AuthenticationData>; this builder ships the simplest plausible
shape and the live-probe iteration will reconcile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9b8133f725 |
[M5] mxaccess-asb: F25 step 5 — KeepAlive + Read + one-way client ops
Extends AsbClient with one-way operation support (`IsOneWay = true`
in IASBIDataV2) plus the KeepAlive and Read operations.
Client API additions:
* `send_envelope_one_way(env)` — frames in SizedEnvelope, writes,
returns immediately. No response read. Mirrors WCF's IsOneWay
semantics for KeepAlive / Disconnect / AuthenticateMe.
* `send_signed_envelope_one_way(action, body, force_hmac)` —
one-way variant that runs the body through F23's authenticator
signing path so the ConnectionValidator header is attached.
* `keep_alive()` — sends an empty `KeepAliveRequest` with default
signing. Used to keep the channel alive past the WCF inactivity
timeout (30s default at `MxAsbDataClient.cs:683`).
* `read(items)` — sends a signed Read envelope, decodes
ReadResponse with both Status and Values arrays.
Operations API additions:
* `build_keep_alive_request_body()` — empty wrapper element +
asb.contracts.messages namespace. Mirror of `AsbContracts.cs:117`
(`public sealed class KeepAlive : ConnectedRequest;`).
* `ReadResponse { status: Vec<ItemStatus>, values: Vec<RuntimeValue> }`
per `AsbContracts.cs:169-179`.
* `decode_read_response(body_tokens)` — pulls both ASBIData
payloads, decodes Status as ItemStatus[], decodes Values via
`decode_runtime_value_array` (4-byte int32 count + per-element
`RuntimeValue::decode` from F24).
5 new tests:
* KeepAlive body shape (empty wrapper, correct namespace).
* ReadResponse decoder round-trip with both Status and Values.
* ReadResponse decoder graceful handling when Values is absent
(returns empty vec).
* End-to-end client::keep_alive — peer drains SizedEnvelope but
doesn't respond; client returns Ok().
* End-to-end client::read — peer responds with synthetic
ReadResponse, client recovers Values[0].timestamp_binary == 1234
and Values[0].status round-trip.
Stubbed for next F25 iterations:
* AsbClient::connect — DH Connect + AuthenticateMe handshake. Needs
ConnectRequest / ConnectResponse builders (regular WCF XML, not
the IAsbCustomSerializableType fast-path).
* Write / PublishWriteComplete / CreateSubscription /
AddMonitoredItems / Publish / Disconnect operation wrappers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1e59249662 |
[M5] mxaccess-asb: F25 step 4 — AsbClient async network loop
The first slice of F25 that actually moves bytes across a transport. Wraps every M5 framing layer (F19-F25.3) into a single async client generic over `AsyncRead + AsyncWrite + Unpin + Send`. Tested in-memory via `tokio::io::duplex` — no live ASB endpoint required. API: * `AsbClient::new(stream, authenticator, via_uri)` — wraps a Tokio transport + F23 authenticator into a ready client. * `send_preamble()` — writes the canonical preamble (Version 1.0 → Duplex → Via → BinaryWithDictionary → PreambleEnd) and reads the peer's PreambleAck. Surfaces Fault as `ClientError::Fault(msg)`. * `send_envelope(env)` — frames `SoapEnvelope` in a SizedEnvelope NMF record, writes, reads the response SizedEnvelope, decodes back to `DecodedEnvelope`. * `send_signed_envelope(action, body, force_hmac)` — calls F23 authenticator's `sign` on the unsigned body bytes, attaches a ConnectionValidator header (base64'd MAC + IV), sends. * `register_items` / `unregister_items` — thin per-operation wrappers threading body builder + response decoder. * `send_end()` — writes record 0x07 + shutdowns the stream. Async record reader: streaming decode of the multibyte-int31 length prefix for SizedEnvelope (0x06) / Fault (0x08), plus a fallback path for Version / Mode / KnownEncoding / etc. `ClientError` covers I/O, NMF, NBFX, Envelope, Operation, Auth, plus PreambleNotSent / AlreadyClosed / Fault / PeerClosed / UnexpectedRecord guards. 6 new tests via in-memory `tokio::io::duplex`: * Preamble round-trip with synthetic peer returning PreambleAck. * Fault propagation through preamble exchange. * End-to-end RegisterItems request → response with a peer that drains preamble, replies PreambleAck, drains the SizedEnvelope, responds with a synthetic RegisterItemsResponse body containing a binary-encoded ItemStatus array. Client decodes and asserts the recovered ItemIdentity name. * `send_envelope` before preamble fails with PreambleNotSent. * `send_end` writes record 0x07 to the wire. * PreambleMode re-export keeps shape parity with `nmf::NmfMode`. Known limitation: the signing path currently hashes the NBFX-encoded body; .NET hashes the XML-text `request.ToXml()`. Functionally present (validator built and attached) but MAC bytes won't match .NET's MAC for the same payload until the live-probe iteration reconciles which canonical form to sign. Stubbed for next F25 iteration: * `AsbClient::connect` — DH `Connect` + `AuthenticateMe` handshake flow. Needs ConnectRequest/Response builders (regular WCF XML, not the IAsbCustomSerializableType fast-path) and the `AsbAuthenticator::create_authentication_data` integration. * Read / Write / Subscription operation wrappers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |