[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:
@@ -34,7 +34,7 @@ use std::time::Duration;
|
||||
|
||||
use mxaccess::AsbTransport;
|
||||
use mxaccess_asb::ItemIdentity;
|
||||
use mxaccess_asb_nettcp::auth::CryptoParameters;
|
||||
use mxaccess_asb_nettcp::auth::{CryptoParameters, HashAlgorithm};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -48,10 +48,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
eprintln!("connecting ASB at {} via {} ...", env.addr, env.via_uri);
|
||||
let connection_id = generate_connection_id();
|
||||
// Each AVEVA install picks its own DH group at install time and
|
||||
// stores it under HKLM\SOFTWARE\Wow6432Node\ArchestrA\
|
||||
// ArchestrAServices\<solution>\{prime,generator,hashAlgorithm,
|
||||
// keySize}. `CryptoParameters::defaults` falls back to the .NET
|
||||
// reference's 1024-bit default — fine for unit tests but will not
|
||||
// match a live AVEVA install (768-bit primes are typical). The
|
||||
// companion loader `tools/Get-AsbPassphrase.ps1` exports the
|
||||
// registry-stored values as MX_ASB_DH_* env vars; if they're set,
|
||||
// honour them.
|
||||
let crypto = build_crypto_parameters_from_env();
|
||||
let (mut transport, response) = AsbTransport::connect(
|
||||
env.addr,
|
||||
&env.passphrase,
|
||||
&CryptoParameters::defaults(),
|
||||
&crypto,
|
||||
&env.via_uri,
|
||||
connection_id,
|
||||
)
|
||||
@@ -157,3 +167,42 @@ fn generate_connection_id() -> [u8; 16] {
|
||||
rand::thread_rng().fill_bytes(&mut bytes);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Build `CryptoParameters` from `MX_ASB_DH_*` env vars, falling back
|
||||
/// to `CryptoParameters::defaults()` for any missing field. Each
|
||||
/// AVEVA install stores its own DH group (prime, generator, hash,
|
||||
/// key-size) under
|
||||
/// `HKLM\SOFTWARE\Wow6432Node\ArchestrA\ArchestrAServices\<solution>\`;
|
||||
/// the companion loader `tools/Get-AsbPassphrase.ps1` exports those
|
||||
/// values so the live-bring-up example doesn't have to read the
|
||||
/// registry directly (which would pull in a Windows-only crate dep
|
||||
/// for what is supposed to be a portable example).
|
||||
fn build_crypto_parameters_from_env() -> CryptoParameters {
|
||||
let mut params = CryptoParameters::defaults();
|
||||
if let Ok(prime) = std::env::var("MX_ASB_DH_PRIME") {
|
||||
params.prime_decimal = prime;
|
||||
}
|
||||
if let Ok(generator) = std::env::var("MX_ASB_DH_GENERATOR") {
|
||||
params.generator_decimal = generator;
|
||||
}
|
||||
if let Ok(hash) = std::env::var("MX_ASB_DH_HASH_ALGORITHM") {
|
||||
// Empty / unrecognised maps to `Unrecognised`, NOT to the
|
||||
// library default. .NET's `AsbSystemAuthenticator.CreateHmac`
|
||||
// (`AsbSystemAuthenticator.cs:84-93`) treats an empty
|
||||
// hashAlgorithm registry value as "fall through to forceHmac
|
||||
// path" (HMAC-SHA1 for AuthenticateMe). Our `Unrecognised`
|
||||
// variant has matching semantics (`auth.rs:303-309`).
|
||||
params.hash_algorithm = match hash.to_ascii_lowercase().as_str() {
|
||||
"md5" => HashAlgorithm::Md5,
|
||||
"sha1" => HashAlgorithm::Sha1,
|
||||
"sha512" => HashAlgorithm::Sha512,
|
||||
_ => HashAlgorithm::Unrecognised,
|
||||
};
|
||||
}
|
||||
if let Ok(size) = std::env::var("MX_ASB_DH_KEY_SIZE") {
|
||||
if let Ok(parsed) = size.parse::<u32>() {
|
||||
params.key_size_bits = parsed;
|
||||
}
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user