Files
mxaccess/rust/crates/mxaccess-asb/tests/fixtures/signed-xml/README.md
T
Joseph Doherty dbb580b2c8 [M5] tools+fixtures: F28 canonical-XML signing target captured from .NET
Adds `MxAsbClient.Probe --dump-signed-xml` flag that builds five
ConnectedRequest shapes (AuthenticateMe, Disconnect, KeepAlive,
RegisterItemsRequest, UnregisterItemsRequest) with deterministic
field values and prints `AsbSerialization.ToXml(...)` output. The
output is exactly what `AsbSystemAuthenticator.Sign` HMACs
(`AsbSystemAuthenticator.cs:79`), so the Rust port's canonical-XML
emitter must produce byte-identical bytes for HMAC parity.

Captured fixtures land under
`rust/crates/mxaccess-asb/tests/fixtures/signed-xml/`:
- `authenticate-me.xml` — 1000 bytes
- `disconnect.xml` — 980 bytes
- `keep-alive.xml` — 705 bytes
- `register-items.xml` — 1068 bytes
- `unregister-items.xml` — 1072 bytes

Plus a `README.md` documenting 10 inferred XmlSerializer rules
(element name = class name not WrapperName, field order =
declaration order not [MessageBodyMember.Order], `[XmlType.Namespace]`
on field type causes per-child xmlns redeclaration on the children
not the wrapper, `*Specified` pattern controls Xxx emission, CRLF +
2-space indent + utf-16 declaration but UTF-8 bytes fed to HMAC).

`.gitattributes` marks the XML fixtures as binary (`*.xml -text`)
so neither `core.autocrlf` nor `text` filters can rewrite the byte
content — CRLF is part of the canonical form and must survive
round-trip through Git untouched.

`MxAsbClient.csproj` gains `<InternalsVisibleTo Include="MxAsbClient
.Probe" />` so the probe can reach the internal `AsbSerialization`
helper without making it public.

Workspace: 702 tests pass (no Rust changes — fixtures only).
F28 follow-up updated with the captured fixtures + the inferred rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 16:35:45 -04:00

4.0 KiB

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