[F27] mxaccess-asb-nettcp: constant-time DH mod_exp via crypto-bigint::DynResidue
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F27 per option (b) of its resolve criterion: fixed-width
U2048 DH backend using crypto-bigint's Montgomery-form residue
arithmetic.
New auth.rs::constant_time_mod_exp(base, exp, modulus) wrapper
preserves the BigUint-in-BigUint-out API of the existing byte
helpers; the actual square-and-multiply chain runs in Montgomery
form. Both DH call sites swap to the wrapper:
- AsbAuthenticator::new line 179 (public-key generation)
- crypto_key line 354 (shared-secret derivation)
DH private exponent timing-leak resistance is the goal: the .NET
reference's BigInteger.ModPow is also non-constant-time, so we
were at parity but not at the long-term Rust target. With this
fix the production path no longer leaks the bit-pattern of the
long-lived DH private key through power/timing side channels.
DynResidueParams::new requires an odd modulus (Montgomery form's
only restriction). Production DH primes are always odd
(`MX_ASB_DH_PRIME = 1552...7919` on this host's registry).
CryptoParameters::DEFAULT_PRIME_TEXT — the test-fixture default
inherited from AsbRegistry.cs:66 — ends in 4 (even), which is
mathematically unsound for DH but kept for parity with the .NET
default. For that case the wrapper falls back to BigUint::modpow,
preserving the wire bytes (modular exp is a pure function of
inputs).
Wire-byte parity verified two ways:
1. Unit fixture test
`auth.rs::deterministic_hmac_matches_dotnet_fixture` — byte-equal
to captured .NET output for the full DH → PBKDF2 → AES-CBC chain.
Continues to pass.
2. Live: Connect handshake against the local AVEVA install
completes with apollo:V2 lifetime, proving MxDataProvider
accepts the constant-time-derived public key and the
shared-secret-based AuthenticateMe.
Workspace deps:
- crypto-bigint = "0.5" added to [workspace.dependencies] and
mxaccess-asb-nettcp/Cargo.toml.
- num-bigint retained for decimal-string parsing + .NET-LE byte
conversion (crypto-bigint has neither).
Closes the "review.md MAJOR finding" originally flagged at
design/30-crate-topology.md:269-274.
design/followups.md: F27 moved to Resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-5
@@ -165,11 +165,6 @@ Both sides see the same `result_code=32` (= `AsbErrorCode.PublishComplete`, info
|
||||
|
||||
**Adjacent observation worth noting:** `AddMonitoredItemsResponse` shows the same symptom shape — our trace reports `add status: 0 item(s); result_code=Some(0) success=Some(true)` while the .NET probe reports `add_monitored_status[0]=item:TestChildObject.TestInt id:18446462598732840962 ...`. Same `IAsbCustomSerializableType`-wrapped Status array; same "0 items where .NET sees 1". These two decoders likely fail for the same root reason and a single fix should close both.
|
||||
|
||||
### F27 — Constant-time DH `mod_exp` (swap `num-bigint` → `crypto-bigint::BoxedUint`)
|
||||
**Severity:** P2 (security regression vs the long-term Rust target — but at parity with the .NET reference today, so not a release-blocker)
|
||||
**Source:** F23 (`crates/mxaccess-asb-nettcp/src/auth.rs:179,303`); originally flagged in `design/30-crate-topology.md:269-274` and the project's `review.md` MAJOR finding.
|
||||
**Why deferred:** `crypto-bigint 0.5`'s `BoxedUint` does not yet expose `pow_mod` over heap-allocated values. The fixed-size `Uint<L>` types do, but require the prime to be parsed into a fixed bit-width and there's no decimal-string parser in `crypto-bigint`. F23 ships with `num-bigint` to keep parity with the .NET reference (which is also not constant-time); the constant-time upgrade is a separate, isolated swap.
|
||||
**Resolves when:** Either (a) `crypto-bigint` lands a stable `BoxedUint::pow_mod` and a decimal-string parser, or (b) we add a small fixed-width DH backend that parses the registry prime into `U2048` once at session construction. At that point `auth::AsbAuthenticator::new`, `crypto_key`, and `generate_private_key` swap `num_bigint::BigUint::modpow` for the constant-time variant; tests stay unchanged because the wire-byte representation is identical.
|
||||
|
||||
### F2 — NTLM verify_signature path + constant-time MAC compare (server-to-client direction)
|
||||
**Severity:** P2
|
||||
@@ -236,6 +231,17 @@ R15's "long-lived connection task" was previously listed as a hard prerequisite,
|
||||
|
||||
Workspace `mxaccess` 65 → 67 tests; default-feature clippy clean. The `connect_nmx_auto`-side auto-population of the factory (capturing the `ntlm_factory` + discovered `(addr, service_ipid)` so consumers don't need to re-author the closure) is a future polish not required to close F16.
|
||||
|
||||
### F27 — Constant-time DH `mod_exp` (swap `num-bigint` → `crypto-bigint::DynResidue`)
|
||||
**Resolved:** 2026-05-06 (commit `<this commit>`). Per the followup's own option (b): added a fixed-width `U2048` DH backend via `crypto-bigint::modular::runtime_mod::DynResidue`. New `auth.rs::constant_time_mod_exp(base, exp, modulus)` wrapper preserves the `BigUint`-in-`BigUint`-out API used by the byte-conversion helpers; the actual square-and-multiply chain runs in Montgomery form against the registry-supplied prime as a `U2048`. Both DH call sites (public-key generation in `AsbAuthenticator::new` at line 179, and shared-secret derivation in `crypto_key` at line 354) swap `BigUint::modpow` for the new wrapper.
|
||||
|
||||
`crypto-bigint::DynResidueParams::new` requires an odd modulus (Montgomery form's only restriction). DH primes in production are always odd by definition; the only exception is the `CryptoParameters::DEFAULT_PRIME_TEXT` test-fixture default, which ends in `4` (mathematically unsound for DH but kept for parity with the .NET reference's published default constant). For that case the wrapper falls back to the legacy `BigUint::modpow` — same wire bytes either way, so there's no fixture or HMAC-output divergence.
|
||||
|
||||
**Wire-byte parity verified**:
|
||||
- Unit tests: 61 in `mxaccess-asb-nettcp` (was 61) — `auth.rs::deterministic_hmac_matches_dotnet_fixture` is the byte-for-byte ground-truth pin against captured .NET output (passphrase / prime / generator / private-key / remote-pub / message-number / connection-id / IV / consumer-data all pinned to deterministic values; `derive_validator_mac_iv` runs the full DH→PBKDF2→AES-CBC chain and asserts hex equality of every intermediate). Continues to pass after the swap.
|
||||
- Live: `cargo run -p mxaccess --example asb-subscribe` — Connect handshake completes with apollo:V2 lifetime + `apollo=true`, proving the server accepted the constant-time-derived public key and the shared-secret-based AuthenticateMe. Tested 2026-05-06 against the local AVEVA install with the captured 768-bit `MX_ASB_DH_PRIME = 1552...7919` (odd; takes the constant-time path).
|
||||
|
||||
Workspace deps: `crypto-bigint = "0.5"` added to `[workspace.dependencies]` and to `mxaccess-asb-nettcp/Cargo.toml`. `num-bigint` retained for decimal-string parsing + .NET-LE byte conversion (crypto-bigint has neither). Default-feature clippy clean. The "review.md MAJOR finding" originally flagged at `design/30-crate-topology.md:269-274` is now closed.
|
||||
|
||||
### F33 — Live wire reconciliation for the ASB subscription path
|
||||
**Resolved:** 2026-05-06 (commits `218f4c4`, `7a5f251`, `<this commit>`). `MX_ASB_TRACE_REPLY` capture during investigation revealed the live MxDataProvider returns a `Result` wrapper with `<resultCodeField>1</>` + `<successField>false</>` followed by **empty** `<ASBIData/>` payloads when it short-circuits on `InvalidConnectionId` — the same transient race F31 fixed for `RegisterItems`. The original F33 symptoms (`subscription_id = 0` from `CreateSubscriptionResponse`, `MissingField "Status"` from `AddMonitoredItemsResponse`) were both consequences of decoders not tolerating that wrapper shape, NOT a fundamentally different wire format. Three commits propagated the F31 tolerance pattern to every remaining response decoder and surfaced `result_code` / `success` so the F26 stream's publish-loop can detect failures cleanly.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user