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>
Signed-request XML fixtures
Canonical XmlSerializer output for every ConnectedRequest shape that
the .NET reference HMACs in AsbSystemAuthenticator.Sign
(src/MxAsbClient/AsbSystemAuthenticator.cs:79). The Rust port's
canonical-XML emitter (F28) must produce these exact UTF-8 bytes for
the HMAC to match the server's recomputation.
Capture procedure
dotnet run --project src\MxAsbClient.Probe -c Release -- --dump-signed-xml > capture.txt
The probe's --dump-signed-xml flag (added 2026-05-05) builds each
shape with deterministic field values and prints the output of
AsbSerialization.ToXml(...) (src/MxAsbClient/AsbSerialization.cs:12).
Pinned values
All shapes use the same ConnectionValidator:
ConnectionId = 8cba964a-74c1-ef74-f6aa-761b3540191bMessageNumber = 42MessageAuthenticationCode = AAECAwQFBgcICQoLDA0ODw==(base64 of bytes 0..15)SignatureInitializationVector = EBESExQVFhcYGRobHB0eHw==(base64 of bytes 16..31)
AuthenticateMe and Disconnect use AuthenticationData with:
Data = "deterministic-ciphertext-bytes"(base64-encoded)InitializationVector = "0123456789abcdef"(base64-encoded)
RegisterItemsRequest uses one ItemIdentity with
Type = Name (0), ReferenceType = Absolute (1),
Name = "TestChildObject.TestInt", ContextName = "".
UnregisterItemsRequest uses one ItemIdentity with
Type = Id (1), ReferenceType = Absolute (1), Name = null,
ContextName = null, Id = 0xCAFEBABEDEADBEEF (14627333968688430831),
IdSpecified = true.
Observed serialiser behaviour
These rules were inferred from the captured output and from the .NET
source for XmlSerializer:
-
Element name = class name, NOT
[MessageContract.WrapperName].XmlSerializerdoes not honour WCF's MessageContract attributes. -
Top-element xmlns ordering (after
<?xml ... ?>):xmlns:xsi, thenxmlns:xsd, then defaultxmlns. TheAsbSerialization.ToXmlpost-process (AsbSerialization.cs:36-47) reparses withXDocument.Loadand reorders to putxsibeforexsd—XmlSerializer's native order is the opposite. -
Field order = C# declaration order (with inherited fields first), NOT
[MessageBodyMember.Order]. -
[XmlType(Namespace = ...)]on a field's type triggers anxmlns="..."redeclaration on EACH child element of that type's instance, NOT on the wrapper element itself. e.g. inside<ConnectionValidator>, every direct child getsxmlns="http://asb.contracts.data/20111111". -
byte[]fields serialise as base64 text content.Guidas canonical lowercase D-format (8cba964a-74c1-...).ulongas decimal.boolas"true"/"false". -
Null reference-type fields with
[XmlElement(IsNullable = true)]produce<Name xsi:nil="true" xmlns="..." />. Empty string fields produce a self-closing<ContextName xmlns="..." />. -
*Specifiedpattern: a public bool field namedXxxSpecified=truecauses XmlSerializer to emit the corresponding<Xxx>element.IdSpecified = false(default) →<Id>omitted.IdSpecified = true→<Id>emitted with the int value. The*Specifiedfield itself is[XmlIgnore]and never emitted. -
Self-closing elements use
/>(space before/>). -
Indentation: 2 spaces,
\r\nline endings, no trailing newline after the closing tag. -
XML declaration:
<?xml version="1.0" encoding="utf-16"?>— noteutf-16even thoughAsbSystemAuthenticator.SignHMACsEncoding.UTF8.GetBytes(...)of this string. The declaration is a static .NET StringWriter default; the actual byte encoding fed to HMAC is UTF-8.
Files
authenticate-me.xml—AuthenticateMeauthenticate-me-empty-mac-iv.xml—AuthenticateMewith the pre-signing validator (empty MAC + IV) — the actual HMAC input shape.disconnect.xml—Disconnectkeep-alive.xml—KeepAliveregister-items.xml—RegisterItemsRequestunregister-items.xml—UnregisterItemsRequest
The eight remaining ConnectedRequest shapes added 2026-05-06 (F28
step 2) cover the data-plane + subscription ops:
read-request.xml—ReadRequestwrite-basic-request.xml—WriteBasicRequestpublish-write-complete-request.xml—PublishWriteCompleteRequestcreate-subscription-request.xml—CreateSubscriptionRequestdelete-subscription-request.xml—DeleteSubscriptionRequestadd-monitored-items-request.xml—AddMonitoredItemsRequestdelete-monitored-items-request.xml—DeleteMonitoredItemsRequestpublish-request.xml—PublishRequest
Pinned values for the new shapes (in addition to the
ConnectionValidator above):
SubscriptionId = 0x1234_5678_9abc_def0(decimal1311768467463790320)MaxQueueSize = 100,SampleInterval = 1000WriteHandle = 0xDEAD_BEEF(decimal3735928559)WriteBasicRequestuses oneWriteValuewhoseValueisVariant.FromInt32(42)(Type=4,Length=4,Payload=[42, 0, 0, 0])AddMonitoredItemsRequestuses oneMonitoredItemwithItem = "TestChildObject.TestInt"by name +SampleInterval=1000+Buffered=false(other fields default)DeleteMonitoredItemsRequestuses oneMonitoredItemwithItem.Id = 0xCAFE_BABE_DEAD_BEEF(the sameIdSpecifiedshape asunregister-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).