diff --git a/design/followups.md b/design/followups.md index 7638462..e4fd51d 100644 --- a/design/followups.md +++ b/design/followups.md @@ -167,29 +167,32 @@ Both sides see the same `result_code=32` (= `AsbErrorCode.PublishComplete`, info ### F2 — NTLM verify_signature path + constant-time MAC compare (server-to-client direction) -**Severity:** P2 -**Source:** M2 wave 1, `crates/mxaccess-rpc/src/ntlm.rs` -**Why deferred:** The .NET `ManagedNtlmClientContext` only implements client-to-server signing (`cs:30,124`); there is no implementation of server-to-client sign/seal keys or `verify_signature`. Both are needed when the callback exporter receives a signed inbound frame from `NmxSvc.exe`, but no such fixture exists yet. -**Resolves when:** M2 wave 3 (callback exporter) captures an `INmxSvcCallback::StatusReceived` frame with an `auth_value` trailer per `design/60-roadmap.md:56` (DoD #3) and a fixture lands under `tests/fixtures/m2-status-frame/`. Add `subtle = "2"` and gate the byte compare behind `ConstantTimeEq` at the same time. +**Severity:** P2 — defensive hardening; the inbound auth-value trailer is currently not validated, but in a typical M4 deployment the callback exporter is bound to localhost and only `NmxSvc.exe` writes to it (no MITM surface inside the box). +**Status:** Awaiting wire-fixture capture. +**Source:** M2 wave 1, `crates/mxaccess-rpc/src/ntlm.rs`. The .NET `ManagedNtlmClientContext` only implements client-to-server signing (`cs:30,124`); there's no implementation of server-to-client sign/seal keys or `verify_signature`. Both are needed when the callback exporter receives a signed inbound frame from `NmxSvc.exe`. +**Concrete next step**: with M2 wave 3 (callback exporter) closed under F15, the path to capture is now wired: +1. Run `cargo run -p mxaccess --example subscribe` (or any consumer that drives `Session::subscribe`) against a live AVEVA install with a real attribute that ticks (`TestChildObject.TestInt` works). +2. Add a temporary `eprintln!` hex dump in `mxaccess-callback::CallbackExporter`'s inbound-frame path to write the raw DCE/RPC bytes to stderr or a file when an `INmxSvcCallback::StatusReceived` frame arrives. The frame should carry an `auth_value` trailer (last `auth_length` bytes of the PDU per the DCE/RPC `[C706]` PDU layout, after the stub data). +3. Save the trailing bytes (header + stub + auth-value) under `crates/mxaccess-rpc/tests/fixtures/m2-status-frame/01-localhost.bin`. +4. Port `verify_signature` mirroring the existing client-side `ManagedNtlmClientContext::sign` shape but using the **server-to-client** sub-keys (`SealKey_S→C` / `SignKey_S→C`) per `[MS-NLMP]` §3.4.4. Add `subtle = "2"` to the workspace deps and gate the MAC compare via `subtle::ConstantTimeEq`. ### F3 — Cross-domain NTLM Type1/2/3 fixture **Severity:** P2 -**Source:** M2 wave 1, `crates/mxaccess-rpc/src/ntlm.rs` -**Why deferred:** All current NTLM fixtures are single-domain (the local AVEVA install). Tracked separately in `design/70-risks-and-open-questions.md` R8 (P1 risk) and the open-evidence-gaps table. -**Resolves when:** A multi-domain AVEVA test harness lands and a successful cross-domain authenticate round-trip captures Type1/2/3 bytes. Notes: this clears R8. - +**Status:** Permanently out-of-scope on the current dev host (no second AD domain). Resolution requires external infrastructure not available here. +**Source:** M2 wave 1, `crates/mxaccess-rpc/src/ntlm.rs`. All current NTLM fixtures are single-domain (the local AVEVA install). Tracked separately in `design/70-risks-and-open-questions.md` R8 (P1 risk) and the open-evidence-gaps table. +**Concrete next step:** Provision a two-domain Windows lab (e.g. `LAB-A` + `LAB-B` with cross-domain trust + an AVEVA install on `LAB-A` that authenticates a user from `LAB-B`). Run `cargo run -p mxaccess --example connect-write-read` from a `LAB-B`-domain user; capture the NTLM Type1 / Type2 / Challenge / Type3 bytes via `examples/asb-relay.rs` or a Wireshark NTLM filter. Save under `crates/mxaccess-rpc/tests/fixtures/cross-domain-ntlm/`. The existing single-domain Type1/2/3 round-trip tests in `mxaccess-rpc::ntlm` then extend to validate the cross-domain shape (TargetInfo AV pairs differ when crossing domains; specifically `MsvAvDnsTreeName` and `MsvAvDnsComputerName` carry the trusted-domain DNS suffix instead of the local one). Clears R8 in the risks doc. ### F10 — `IObjectExporter::ResolveOxid2` (opnum 4) body codec -**Severity:** P2 -**Source:** M2 wave 2, `crates/mxaccess-rpc/src/object_exporter.rs` -**Why deferred:** `ObjectExporterMessages.cs` only models opnum 0 (`ResolveOxid`). Opnum 4 (`ResolveOxid2`) has a different response shape — it adds a `COMVERSION` plus an `AuthnHnt[]` array. The .NET reference does not exercise this path, so there's no executable spec to mirror. -**Resolves when:** Either a `[MS-DCOM]` §3.1.2.5.1.4-derived layout is verified against a captured `ResolveOxid2` exchange, or the .NET reference grows a `ParseResolveOxid2*` helper. +**Severity:** P2 — the ResolveOxid (opnum 0) path is what the .NET reference + our Rust port use; opnum 4 is only needed by callers that want the additional `COMVERSION` + `AuthnHnt[]` data. +**Status:** Awaiting wire-fixture capture or .NET helper. +**Source:** M2 wave 2, `crates/mxaccess-rpc/src/object_exporter.rs`. `ObjectExporterMessages.cs` only models opnum 0; opnum 4 has a different response shape per `[MS-DCOM]` §3.1.2.5.1.4. No .NET executable spec to mirror. +**Concrete next step:** Either (a) extend `MxNativeClient.Probe` with a `--probe-resolve-oxid2` flag that calls `IObjectExporter::ResolveOxid2(oxid, &requested_protseqs)` against `localhost:135` and dumps the response stub to a file, then port the layout into `object_exporter.rs::parse_resolve_oxid2_result` mirroring the existing `parse_resolve_oxid_result` (`object_exporter.rs:226`); or (b) hand-roll the layout from `[MS-DCOM]` §3.1.2.5.1.4 (response = same as ResolveOxid + 8-byte `COMVERSION` + 4-byte `AuthnHnt[]` count + N×4-byte ushort entries + 4-byte status), commit the structural codec, and rely on a future captured frame to validate. ### F11 — `IRemUnknown::RemAddRef` and `RemRelease` body codecs -**Severity:** P2 -**Source:** M2 wave 2, `crates/mxaccess-rpc/src/rem_unknown.rs` -**Why deferred:** `RemUnknownMessages.cs` declares the opnums (`:9-10`) but does not implement encoders/decoders. The Rust port matches that exactly per "port what is already proven." -**Resolves when:** The .NET reference adds bodies for opnums 4 / 5 (or a captured frame establishes the on-wire shape). At that point port them into `rem_unknown.rs` alongside the existing `RemQueryInterface` codec. +**Severity:** P2 — neither opnum is exercised by the .NET reference's NMX session lifecycle, so the lack of a body codec doesn't block any current consumer. +**Status:** Awaiting wire-fixture capture or .NET helper. +**Source:** M2 wave 2, `crates/mxaccess-rpc/src/rem_unknown.rs`. `RemUnknownMessages.cs` declares the opnums (`:9-10`) but doesn't implement encoders/decoders. Rust port matches that per "port what is already proven." +**Concrete next step:** Either extend `MxNativeClient.Probe` with `--probe-rem-add-ref` / `--probe-rem-release` flags that exercise opnums 4 and 5 against an existing `IRemUnknown` IPID, capture the responses, and port the body layouts into `rem_unknown.rs` alongside the existing `RemQueryInterface` codec; OR derive the layouts from `[MS-DCOM]` §3.1.1.5.6 (`REMINTERFACEREF[]` array of IPID + public/private refs counts) and ship the codecs structurally.