Files
mxaccess/rust/crates/mxaccess-asb/tests/fixtures/signed-xml
Joseph Doherty 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>
2026-05-05 17:31:31 -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
  • disconnect.xmlDisconnect
  • keep-alive.xmlKeepAlive
  • register-items.xmlRegisterItemsRequest
  • unregister-items.xmlUnregisterItemsRequest

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