[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>
This commit is contained in:
@@ -17,9 +17,18 @@ internal sealed class AsbSystemAuthenticator
|
||||
private readonly byte[] localPublicKey;
|
||||
private byte[] remotePublicKey = [];
|
||||
private ulong nextMessageNumber = 1;
|
||||
/// Trace callback for the F28 canonical-XML reconciliation pass —
|
||||
/// when set, `Sign` dumps the request type, the UTF-8 bytes of
|
||||
/// `request.ToXml()`, the resulting HMAC, and the encrypted MAC +
|
||||
/// IV. Used by `MxAsbClient.Probe --dump-signed-xml` and ad-hoc
|
||||
/// live runs to capture the exact bytes the server's HMAC verifier
|
||||
/// recomputes against; the Rust port's `xml_canonical` emitter must
|
||||
/// produce byte-identical XML for the HMAC to round-trip.
|
||||
private readonly Action<string>? sharedTrace;
|
||||
|
||||
public AsbSystemAuthenticator(string passphrase, AsbSolutionCryptoParameters cryptoParameters, Action<string>? trace = null)
|
||||
{
|
||||
sharedTrace = trace;
|
||||
dhPrime = cryptoParameters.Prime;
|
||||
dhGenerator = cryptoParameters.Generator;
|
||||
hashAlgorithm = cryptoParameters.HashAlgorithm;
|
||||
@@ -76,9 +85,17 @@ internal sealed class AsbSystemAuthenticator
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(request.ToXml()));
|
||||
string xmlText = request.ToXml();
|
||||
byte[] xmlBytes = Encoding.UTF8.GetBytes(xmlText);
|
||||
sharedTrace?.Invoke($"asb.sign.type={request.GetType().Name}");
|
||||
sharedTrace?.Invoke($"asb.sign.xml-utf8-len={xmlBytes.Length}");
|
||||
sharedTrace?.Invoke($"asb.sign.xml-b64={Convert.ToBase64String(xmlBytes)}");
|
||||
byte[] hash = hmac.ComputeHash(xmlBytes);
|
||||
sharedTrace?.Invoke($"asb.sign.hmac-b64={Convert.ToBase64String(hash)}");
|
||||
validator.MessageAuthenticationCode = Encrypt(hash, out byte[] iv);
|
||||
validator.SignatureInitializationVector = iv;
|
||||
sharedTrace?.Invoke($"asb.sign.encrypted-mac-b64={Convert.ToBase64String(validator.MessageAuthenticationCode)}");
|
||||
sharedTrace?.Invoke($"asb.sign.iv-b64={Convert.ToBase64String(iv)}");
|
||||
}
|
||||
|
||||
private HMAC? CreateHmac(bool forceHmac)
|
||||
|
||||
Reference in New Issue
Block a user