[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:
@@ -68,6 +68,31 @@ function Resolve-AsbSolutionName {
|
||||
return $default
|
||||
}
|
||||
|
||||
function Get-AsbCryptoParameters {
|
||||
param([string]$Solution)
|
||||
# Read the per-solution `prime`, `generator`, `hashAlgorithm`, and
|
||||
# `keySize` registry values. Each AVEVA install picks its own DH
|
||||
# group at provisioning time, so the Rust port must use the
|
||||
# registry-stored values rather than a hardcoded constant — the
|
||||
# default in `CryptoParameters::defaults` is the .NET reference's
|
||||
# 1024-bit fallback (`AsbRegistry.cs:66-83`), but real installs use
|
||||
# smaller group sizes (768-bit prime is common). Mismatch produces a
|
||||
# working `Connect` (the wire bytes are exchanged) but a broken
|
||||
# `AuthenticateMe` (encrypted ConsumerData decrypts to garbage on
|
||||
# the server side because the shared secret derivation diverges).
|
||||
$path = "$ServicesKeyPath\$Solution"
|
||||
if (-not (Test-Path $path)) {
|
||||
throw "Solution registry key not found at $path."
|
||||
}
|
||||
$key = Get-ItemProperty -Path $path -ErrorAction Stop
|
||||
return [pscustomobject]@{
|
||||
Prime = if ($key.PSObject.Properties['prime']) { $key.prime } else { $null }
|
||||
Generator = if ($key.PSObject.Properties['generator']) { $key.generator } else { $null }
|
||||
HashAlgorithm = if ($key.PSObject.Properties['hashAlgorithm']) { $key.hashAlgorithm } else { $null }
|
||||
KeySize = if ($key.PSObject.Properties['keySize']) { $key.keySize } else { $null }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AsbSharedSecretBytes {
|
||||
param([string]$Solution)
|
||||
$path = "$ServicesKeyPath\$Solution"
|
||||
@@ -147,6 +172,34 @@ Set-LiveEnvVar -Name 'MX_ASB_SOLUTION' -Value $solution
|
||||
Set-LiveEnvVar -Name 'MX_ASB_GALAXY_NAME' -Value $GalaxyName
|
||||
Set-LiveEnvVar -Name 'MX_ASB_PASSPHRASE' -Value $passphrase -Sensitive
|
||||
|
||||
# Per-solution DH crypto parameters from the registry — must override
|
||||
# the Rust port's hardcoded `CryptoParameters::defaults()` (which uses
|
||||
# the .NET reference's 1024-bit default; real installs use whatever
|
||||
# was provisioned at install time, often a smaller 768-bit prime).
|
||||
$crypto = Get-AsbCryptoParameters -Solution $solution
|
||||
if ($crypto.Prime) {
|
||||
# Strip whitespace/newlines that PowerShell display would wrap into
|
||||
# the shown value; the registry-stored decimal must be a single
|
||||
# contiguous integer.
|
||||
$primeClean = $crypto.Prime -replace '\s+', ''
|
||||
Set-LiveEnvVar -Name 'MX_ASB_DH_PRIME' -Value $primeClean
|
||||
} else {
|
||||
Write-Host "[WARN] no `prime` value in registry — leaving Rust default in place" -ForegroundColor Yellow
|
||||
}
|
||||
if ($crypto.Generator) {
|
||||
$genClean = ($crypto.Generator.ToString()) -replace '\s+', ''
|
||||
Set-LiveEnvVar -Name 'MX_ASB_DH_GENERATOR' -Value $genClean
|
||||
}
|
||||
# Always export, even if empty — empty string in the registry means
|
||||
# "use the forceHmac fallback (HMAC-SHA1)" per `AsbSystemAuthenticator
|
||||
# .cs:91-92`. The example must distinguish "no env var" (use library
|
||||
# default, MD5) from "registry says empty" (Unrecognised → SHA1 when
|
||||
# forced). We pick the empty-string sentinel.
|
||||
Set-LiveEnvVar -Name 'MX_ASB_DH_HASH_ALGORITHM' -Value ($crypto.HashAlgorithm ?? '')
|
||||
if ($crypto.KeySize) {
|
||||
Set-LiveEnvVar -Name 'MX_ASB_DH_KEY_SIZE' -Value ($crypto.KeySize.ToString())
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Done. Run the example with:' -ForegroundColor Green
|
||||
Write-Host ' cargo run -p mxaccess --example asb-subscribe' -ForegroundColor DarkGray
|
||||
|
||||
Reference in New Issue
Block a user