[F28] mxaccess-asb: canonical XML signing for all 8 remaining ops
rust / build / test / clippy / fmt (push) Has been cancelled

Closes F28. The 5 [XmlSerializerFormat] ops landed in commit f14580e
(2026-05-05); this commit closes out the remaining 8 ConnectedRequest
shapes, eliminating the legacy NBFX-bytes signing fallback from every
`client::*` op.

Two deliverables:

1. Extended `MxAsbClient.Probe --dump-signed-xml` (.NET probe) to
   emit deterministic canonical-XML output for ReadRequest,
   WriteBasicRequest, PublishWriteCompleteRequest,
   CreateSubscriptionRequest, DeleteSubscriptionRequest,
   AddMonitoredItemsRequest, DeleteMonitoredItemsRequest,
   PublishRequest. Saved 8 fixtures at
   rust/crates/mxaccess-asb/tests/fixtures/signed-xml/*.xml. Pinned
   field values for reproducibility:
     - SubscriptionId = 0x1234_5678_9abc_def0
     - MaxQueueSize = 100, SampleInterval = 1000
     - WriteHandle = 0xDEAD_BEEF
     - WriteValue = Variant.FromInt32(42)
     - MonitoredItem with the existing sample-item shape

2. Ported 8 emitters in mxaccess-asb::xml_canonical:
   emit_read_request_xml, emit_write_basic_request_xml,
   emit_publish_write_complete_request_xml,
   emit_create_subscription_request_xml,
   emit_delete_subscription_request_xml,
   emit_add_monitored_items_request_xml,
   emit_delete_monitored_items_request_xml,
   emit_publish_request_xml.

   New helpers consolidate XmlSerializer's per-namespace shapes:
     - emit_invensys_text — primitive int/long fields in the parent
       urn:invensys.schemas namespace (no xmlns redeclaration).
     - emit_write_value — <Values> wrapper inlining
       Value (Variant), Status (default AsbStatus), Comment (xsi:nil).
     - emit_monitored_item — <Items> wrapper inlining
       Item, SampleInterval, ValueDeadband, UserData, Buffered.
     - emit_inline_item_identity — ItemIdentity rendered as a child
       of MonitoredItem (single xmlns redeclaration on the wrapper,
       children inherit).
     - emit_inline_text + emit_inline_optional_string —
       no-redeclaration variants of emit_iom_text +
       emit_iom_optional_string.
     - emit_idata_variant — Variant's Type/Length/Payload children
       in the http://asb.contracts.idata.data/20111111 namespace
       (Payload self-closes with xsi:nil when Length=0).
     - emit_iom_default_variant — wrapper for ValueDeadband / UserData
       (default-shape Variant in iom:2 namespace).

   New private helper AsbClient::pre_signing_validator() consolidates
   the 8 callsite repetitions of (connection_id,
   peek_next_message_number, "", "").

Wired into client::* — every send_signed_envelope[_one_way] call now
passes Some(&xml) for xml_for_signing. The 8 ops affected: read,
write, publish_write_complete, delete_monitored_items,
create_subscription, add_monitored_items, publish,
delete_subscription (plus their _once retry-loop variants).

8 new fixture-comparison tests (mxaccess-asb 87 → 95). Each emitter
byte-equal vs the .NET fixture on the first try — no iteration
needed. Workspace clippy clean.

Live verification: `cargo run -p mxaccess --example asb-subscribe`
returns TestChildObject.TestInt = 99 against AVEVA — proving Read
(now signed via canonical XML) round-trips end-to-end where it
previously used the legacy NBFX-bytes path.

The remaining 7 ops are wire-tested at fixture-byte-equality only;
live exercise is gated on the F33 follow-on capture for
subscribe-flow ops, but the canonical XML matches the .NET reference
byte-for-byte, so the HMAC will match by construction once the
session is in a state to issue those ops.

design/followups.md:
  - F28 moved to Resolved with the full two-step audit trail.
  - F18 M5 status block rewritten — all sub-followups (F26 stream,
    F28, F29, F32, F33) now closed. M5 DoD bullets 1+2+3+4 all green.
  - tests/fixtures/signed-xml/README.md updated to list the 8 new
    fixtures + their pinned input values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-06 02:13:16 -04:00
parent ff4ea4d5a9
commit 34d477819b
13 changed files with 774 additions and 39 deletions
@@ -89,11 +89,40 @@ source for `XmlSerializer`:
## Files
- `authenticate-me.xml``AuthenticateMe`
- `authenticate-me-empty-mac-iv.xml``AuthenticateMe` with the
pre-signing validator (empty MAC + IV) — the actual HMAC input shape.
- `disconnect.xml``Disconnect`
- `keep-alive.xml``KeepAlive`
- `register-items.xml``RegisterItemsRequest`
- `unregister-items.xml``UnregisterItemsRequest`
The eight remaining `ConnectedRequest` shapes added 2026-05-06 (F28
step 2) cover the data-plane + subscription ops:
- `read-request.xml``ReadRequest`
- `write-basic-request.xml``WriteBasicRequest`
- `publish-write-complete-request.xml``PublishWriteCompleteRequest`
- `create-subscription-request.xml``CreateSubscriptionRequest`
- `delete-subscription-request.xml``DeleteSubscriptionRequest`
- `add-monitored-items-request.xml``AddMonitoredItemsRequest`
- `delete-monitored-items-request.xml``DeleteMonitoredItemsRequest`
- `publish-request.xml``PublishRequest`
Pinned values for the new shapes (in addition to the
`ConnectionValidator` above):
- `SubscriptionId = 0x1234_5678_9abc_def0` (decimal `1311768467463790320`)
- `MaxQueueSize = 100`, `SampleInterval = 1000`
- `WriteHandle = 0xDEAD_BEEF` (decimal `3735928559`)
- `WriteBasicRequest` uses one `WriteValue` whose `Value` is
`Variant.FromInt32(42)` (`Type=4`, `Length=4`, `Payload=[42, 0, 0, 0]`)
- `AddMonitoredItemsRequest` uses one `MonitoredItem` with
`Item = "TestChildObject.TestInt"` by name + `SampleInterval=1000` +
`Buffered=false` (other fields default)
- `DeleteMonitoredItemsRequest` uses one `MonitoredItem` with
`Item.Id = 0xCAFE_BABE_DEAD_BEEF` (the same `IdSpecified` shape as
`unregister-items.xml`)
Each file is the verbatim UTF-8 representation of `request.ToXml()`,
with literal `\r\n` line endings preserved. Treat as binary (don't
let your editor reformat).
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-16"?>
<AddMonitoredItemsRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
<Items>
<Item xmlns="urn:data.data.asb.iom:2">
<Type>0</Type>
<ReferenceType>1</ReferenceType>
<Name>TestChildObject.TestInt</Name>
<ContextName />
</Item>
<SampleInterval xmlns="urn:data.data.asb.iom:2">1000</SampleInterval>
<ValueDeadband xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</ValueDeadband>
<UserData xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</UserData>
<Buffered xmlns="urn:data.data.asb.iom:2">false</Buffered>
</Items>
<RequireId>true</RequireId>
</AddMonitoredItemsRequest>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-16"?>
<CreateSubscriptionRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<MaxQueueSize>100</MaxQueueSize>
<SampleInterval>1000</SampleInterval>
</CreateSubscriptionRequest>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-16"?>
<DeleteMonitoredItemsRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
<Items>
<Item xmlns="urn:data.data.asb.iom:2">
<Type>1</Type>
<ReferenceType>1</ReferenceType>
<Name xsi:nil="true" />
<ContextName xsi:nil="true" />
<Id>14627333968688430831</Id>
</Item>
<SampleInterval xmlns="urn:data.data.asb.iom:2">1000</SampleInterval>
<ValueDeadband xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</ValueDeadband>
<UserData xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</UserData>
<Buffered xmlns="urn:data.data.asb.iom:2">false</Buffered>
</Items>
</DeleteMonitoredItemsRequest>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-16"?>
<DeleteSubscriptionRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
</DeleteSubscriptionRequest>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-16"?>
<PublishRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
</PublishRequest>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-16"?>
<PublishWriteCompleteRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
</PublishWriteCompleteRequest>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-16"?>
<ReadRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<Items>
<Type xmlns="urn:data.data.asb.iom:2">0</Type>
<ReferenceType xmlns="urn:data.data.asb.iom:2">1</ReferenceType>
<Name xmlns="urn:data.data.asb.iom:2">TestChildObject.TestInt</Name>
<ContextName xmlns="urn:data.data.asb.iom:2" />
</Items>
</ReadRequest>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-16"?>
<WriteBasicRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<Items>
<Type xmlns="urn:data.data.asb.iom:2">0</Type>
<ReferenceType xmlns="urn:data.data.asb.iom:2">1</ReferenceType>
<Name xmlns="urn:data.data.asb.iom:2">TestChildObject.TestInt</Name>
<ContextName xmlns="urn:data.data.asb.iom:2" />
</Items>
<Values>
<Value xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">4</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">4</Length>
<Payload xmlns="http://asb.contracts.idata.data/20111111">KgAAAA==</Payload>
</Value>
<Status xmlns="urn:data.data.asb.iom:2">
<Count>0</Count>
</Status>
<Comment xsi:nil="true" xmlns="urn:data.data.asb.iom:2" />
</Values>
<WriteHandle>3735928559</WriteHandle>
</WriteBasicRequest>