34d477819b
rust / build / test / clippy / fmt (push) Has been cancelled
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>
129 lines
5.5 KiB
Markdown
129 lines
5.5 KiB
Markdown
# 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`
|
|
- `authenticate-me-empty-mac-iv.xml` — `AuthenticateMe` with the
|
|
pre-signing validator (empty MAC + IV) — the actual HMAC input shape.
|
|
- `disconnect.xml` — `Disconnect`
|
|
- `keep-alive.xml` — `KeepAlive`
|
|
- `register-items.xml` — `RegisterItemsRequest`
|
|
- `unregister-items.xml` — `UnregisterItemsRequest`
|
|
|
|
The eight remaining `ConnectedRequest` shapes added 2026-05-06 (F28
|
|
step 2) cover the data-plane + subscription ops:
|
|
|
|
- `read-request.xml` — `ReadRequest`
|
|
- `write-basic-request.xml` — `WriteBasicRequest`
|
|
- `publish-write-complete-request.xml` — `PublishWriteCompleteRequest`
|
|
- `create-subscription-request.xml` — `CreateSubscriptionRequest`
|
|
- `delete-subscription-request.xml` — `DeleteSubscriptionRequest`
|
|
- `add-monitored-items-request.xml` — `AddMonitoredItemsRequest`
|
|
- `delete-monitored-items-request.xml` — `DeleteMonitoredItemsRequest`
|
|
- `publish-request.xml` — `PublishRequest`
|
|
|
|
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).
|