[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>
This commit is contained in:
Joseph Doherty
2026-05-05 16:35:45 -04:00
parent d1e887b91b
commit dbb580b2c8
10 changed files with 269 additions and 1 deletions
@@ -0,0 +1,7 @@
# These fixtures are byte-equal targets for the F28 canonical XML
# emitter — `XmlSerializer.Serialize(...)` output that the .NET
# reference HMACs in `AsbSystemAuthenticator.Sign`. CRLF line endings
# are part of the canonical form (StringWriter default on Windows),
# so Git MUST NOT touch them. `-text` marks them as binary so neither
# `core.autocrlf` nor `text` filters can rewrite the bytes.
*.xml -text
@@ -0,0 +1,99 @@
# 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
```powershell
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
`xsd``XmlSerializer`'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.xml``AuthenticateMe`
- `disconnect.xml``Disconnect`
- `keep-alive.xml``KeepAlive`
- `register-items.xml``RegisterItemsRequest`
- `unregister-items.xml``UnregisterItemsRequest`
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,13 @@
<?xml version="1.0" encoding="utf-16"?>
<AuthenticateMe 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>
<ConsumerAuthenticationData>
<Data xmlns="http://asb.contracts.data/20111111">ZGV0ZXJtaW5pc3RpYy1jaXBoZXJ0ZXh0LWJ5dGVz</Data>
<InitializationVector xmlns="http://asb.contracts.data/20111111">MDEyMzQ1Njc4OWFiY2RlZg==</InitializationVector>
</ConsumerAuthenticationData>
</AuthenticateMe>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-16"?>
<Disconnect 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>
<ConsumerAuthenticationData>
<Data xmlns="http://asb.contracts.data/20111111">ZGlzY29ubmVjdC1jaXBoZXJ0ZXh0</Data>
<InitializationVector xmlns="http://asb.contracts.data/20111111">MDEyMzQ1Njc4OWFiY2RlZg==</InitializationVector>
</ConsumerAuthenticationData>
</Disconnect>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-16"?>
<KeepAlive 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>
</KeepAlive>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-16"?>
<RegisterItemsRequest 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>
<RequireId>true</RequireId>
<RegisterOnly>false</RegisterOnly>
</RegisterItemsRequest>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-16"?>
<UnregisterItemsRequest 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">1</Type>
<ReferenceType xmlns="urn:data.data.asb.iom:2">1</ReferenceType>
<Name xsi:nil="true" xmlns="urn:data.data.asb.iom:2" />
<ContextName xsi:nil="true" xmlns="urn:data.data.asb.iom:2" />
<Id xmlns="urn:data.data.asb.iom:2">14627333968688430831</Id>
</Items>
</UnregisterItemsRequest>