Files
mxaccess/rust/crates/mxaccess-asb/tests/fixtures/signed-xml
Joseph Doherty 34d477819b
rust / build / test / clippy / fmt (push) Has been cancelled
[F28] mxaccess-asb: canonical XML signing for all 8 remaining ops
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>
2026-05-06 02:13:16 -04:00
..

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-761b3540191b
  • MessageNumber = 42
  • MessageAuthenticationCode = 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:

  1. Element name = class name, NOT [MessageContract.WrapperName]. XmlSerializer does not honour WCF's MessageContract attributes.

  2. Top-element xmlns ordering (after <?xml ... ?>): xmlns:xsi, then xmlns:xsd, then default xmlns. The AsbSerialization.ToXml post-process (AsbSerialization.cs:36-47) reparses with XDocument.Load and reorders to put xsi before xsdXmlSerializer's native order is the opposite.

  3. Field order = C# declaration order (with inherited fields first), NOT [MessageBodyMember.Order].

  4. [XmlType(Namespace = ...)] on a field's type triggers an xmlns="..." redeclaration on EACH child element of that type's instance, NOT on the wrapper element itself. e.g. inside <ConnectionValidator>, every direct child gets xmlns="http://asb.contracts.data/20111111".

  5. byte[] fields serialise as base64 text content. Guid as canonical lowercase D-format (8cba964a-74c1-...). ulong as decimal. bool as "true" / "false".

  6. Null reference-type fields with [XmlElement(IsNullable = true)] produce <Name xsi:nil="true" xmlns="..." />. Empty string fields produce a self-closing <ContextName xmlns="..." />.

  7. *Specified pattern: a public bool field named XxxSpecified = true causes XmlSerializer to emit the corresponding <Xxx> element. IdSpecified = false (default) → <Id> omitted. IdSpecified = true<Id> emitted with the int value. The *Specified field itself is [XmlIgnore] and never emitted.

  8. Self-closing elements use /> (space before />).

  9. Indentation: 2 spaces, \r\n line endings, no trailing newline after the closing tag.

  10. XML declaration: <?xml version="1.0" encoding="utf-16"?> — note utf-16 even though AsbSystemAuthenticator.Sign HMACs Encoding.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.xmlAuthenticateMe
  • authenticate-me-empty-mac-iv.xmlAuthenticateMe with the pre-signing validator (empty MAC + IV) — the actual HMAC input shape.
  • disconnect.xmlDisconnect
  • keep-alive.xmlKeepAlive
  • register-items.xmlRegisterItemsRequest
  • unregister-items.xmlUnregisterItemsRequest

The eight remaining ConnectedRequest shapes added 2026-05-06 (F28 step 2) cover the data-plane + subscription ops:

  • read-request.xmlReadRequest
  • write-basic-request.xmlWriteBasicRequest
  • publish-write-complete-request.xmlPublishWriteCompleteRequest
  • create-subscription-request.xmlCreateSubscriptionRequest
  • delete-subscription-request.xmlDeleteSubscriptionRequest
  • add-monitored-items-request.xmlAddMonitoredItemsRequest
  • delete-monitored-items-request.xmlDeleteMonitoredItemsRequest
  • publish-request.xmlPublishRequest

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).