feat(server): validate auto-reconnect cookies - #1509
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Not ready to approve
Cookie secrets can leak through debug logging, runtime update failures invalidate usable cookies, and the arbitrary feature does not compile.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Adds server-side validation and rotation for RDP auto-reconnect cookies.
Changes:
- Parses and propagates client reconnect packets.
- Validates HMAC-MD5 verifiers and bypasses credential revalidation for valid reconnects.
- Adds per-connection/hourly rotation and runtime updates.
File summaries
| File | Description |
|---|---|
crates/ironrdp-server/src/server.rs |
Implements validation, rotation, and runtime updates. |
crates/ironrdp-server/src/lib.rs |
Exports the update handle. |
crates/ironrdp-server/src/builder.rs |
Documents lifecycle behavior. |
crates/ironrdp-server/Cargo.toml |
Adds cryptographic and timing dependencies. |
crates/ironrdp-pdu/src/rdp/client_info.rs |
Decodes client reconnect packets. |
crates/ironrdp-acceptor/src/connection.rs |
Propagates reconnect data through acceptance. |
Cargo.lock |
Locks new dependencies. |
Review details
- Files reviewed: 6/7 changed files
- Comments generated: 5
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| /// Replace or clear the Server Auto-Reconnect Cookie. | ||
| SetAutoReconnectCookie(Option<rdp::session_info::ServerAutoReconnect>), |
There was a problem hiding this comment.
Fixed in 546f061. ServerEvent now implements a redacted Debug representation, so cookie and credential payloads cannot appear in event trace logs or SendError debug output.
| ServerEvent::SetAutoReconnectCookie(cookie) => { | ||
| self.set_auto_reconnect_cookie(cookie); | ||
| self.send_next_auto_reconnect_cookie(writer, io_channel_id, user_channel_id) | ||
| .await?; |
There was a problem hiding this comment.
Fixed in 546f061. Runtime replacement now writes the Save Session Info PDU before committing the new cookie state, preserving the prior state if delivery fails.
| /// `None` disables auto-reconnect and immediately invalidates the cookie | ||
| /// currently held by the server. |
There was a problem hiding this comment.
Fixed in 546f061. AutoReconnectCookieHandle::set now explicitly documents that the update is queued and when a None update takes effect.
| session_id: Option<u32>, | ||
| performance_flags: Option<PerformanceFlags>, | ||
| reconnect_cookie: Option<[u8; RECONNECT_COOKIE_LEN]>, | ||
| auto_reconnect: Option<ClientAutoReconnect>, |
There was a problem hiding this comment.
Fixed in 546f061. The builder now parses and stores the matching typed representation alongside the raw cookie, with regression coverage for valid builder input.
|
Marc-André Moreau (@mamoreau-devolutions) heads up on an overlap, plus one thing in the patch that is independent of it. I have two open on this surface: #1496 (reissue the cookie mid-session) and #1501 (client half, session resume). #1501 adds Tell me the merge order you want and I will rebase to fit, including closing either of mine if you would rather this PR carry the whole thing. The acceptor plumbing and the validation path here are yours and I am not looking to duplicate them. Separately, one thing worth a look regardless of the ordering. In optional_data.auto_reconnect = Some(ClientAutoReconnect::decode(&mut ReadCursor::new(&reconnect_cookie))?);That |
|
Given that this particular PR is not tested, just implemented based on the related issue, consider that your other PRs have precedence over mine. I will rebase and resolve conflicts accordingly |
|
Marc-André Moreau (@mamoreau-devolutions) two more I'd found, with the fix instead of the question. Against Rotation retires the old cookie too early. The validator bypass isn't documented. --- a/crates/ironrdp-server/src/server.rs
+++ b/crates/ironrdp-server/src/server.rs
@@ struct RdpServer
auto_reconnect_cookie: Option<rdp::session_info::ServerAutoReconnect>,
+ /// The cookie the current one replaced, still accepted on reconnect.
+ ///
+ /// Sending a replacement only proves the bytes reached the socket, not that
+ /// the client read them. Retiring the old cookie at that moment leaves a
+ /// client that dropped around the rotation holding one the server no longer
+ /// knows, unable to reconnect: precisely the ungraceful disconnect this
+ /// feature exists for. Honouring the previous cookie until the next rotation
+ /// closes that window, at the cost of two live cookies per session.
+ previous_auto_reconnect_cookie: Option<rdp::session_info::ServerAutoReconnect>,
auto_reconnect_sent: bool,
@@ RdpServer::new
auto_reconnect_cookie: None,
+ previous_auto_reconnect_cookie: None,
auto_reconnect_sent: false,
@@ set_auto_reconnect_cookie
self.auto_reconnect_cookie = cookie;
+ // An explicit set is a revocation, unlike a rotation: drop the previous
+ // cookie rather than carrying it over, so `None` really does invalidate
+ // what the server currently honours.
+ self.previous_auto_reconnect_cookie = None;
self.auto_reconnect_sent = false;
@@ verify_auto_reconnect_cookie
- self.auto_reconnect_cookie
- .as_ref()
- .is_some_and(|cookie| auto_reconnect_cookie_matches(cookie, reconnect))
+ // Either the current cookie or the one it replaced: see
+ // `previous_auto_reconnect_cookie` for why the old one stays valid.
+ [&self.auto_reconnect_cookie, &self.previous_auto_reconnect_cookie]
+ .into_iter()
+ .flatten()
+ .any(|cookie| auto_reconnect_cookie_matches(cookie, reconnect))
@@ send_next_auto_reconnect_cookie AND rotate_auto_reconnect_cookie (both sites)
- self.auto_reconnect_cookie = Some(cookie);
+ self.previous_auto_reconnect_cookie = self.auto_reconnect_cookie.replace(cookie);
self.auto_reconnect_sent = true;Plus the same six-line doc note on
|
|
Tested this end-to-end against real mstsc — it works. This is in response to your answer to issue #1508 Rather than wire it into our downstream server (pinned well behind The full auto-reconnect cycle (log): Session resumed cleanly. So a real mstsc's Bonus: the reconnect happened to arrive from a different source IP than the initial connection (a ZeroTier address vs the LAN one) — so validation is correctly IP-independent and survived a network change (roaming client). I also added a known-answer test that pins the verifier construction against an independent Python #[cfg(test)]
mod auto_reconnect_tests {
use ironrdp_pdu::rdp::client_info::ClientAutoReconnect;
use ironrdp_pdu::rdp::session_info::ServerAutoReconnect;
use super::{auto_reconnect_cookie_matches, auto_reconnect_verifier_matches};
// Independently computed (Python hmac/hashlib):
// HMAC-MD5(key = 01..10, msg = 32 zero bytes) = 894025a9…261c
const RANDOM_BITS: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
const EXPECTED_VERIFIER: [u8; 16] = [
0x89, 0x40, 0x25, 0xa9, 0x9d, 0x64, 0xab, 0x96, 0x64, 0x19, 0xec, 0x1e, 0xf1, 0x3c, 0x26, 0x1c,
];
#[test]
fn verifier_matches_reference_hmac_md5() {
assert!(auto_reconnect_verifier_matches(&RANDOM_BITS, &EXPECTED_VERIFIER));
}
#[test]
fn verifier_rejects_a_wrong_value() {
let mut wrong = EXPECTED_VERIFIER;
wrong[0] ^= 0xff;
assert!(!auto_reconnect_verifier_matches(&RANDOM_BITS, &wrong));
}
#[test]
fn cookie_match_requires_both_logon_id_and_verifier() {
let cookie = ServerAutoReconnect { logon_id: 0x1234_5678, random_bits: RANDOM_BITS };
let good = ClientAutoReconnect { logon_id: 0x1234_5678, security_verifier: EXPECTED_VERIFIER };
assert!(auto_reconnect_cookie_matches(&cookie, &good));
let wrong_id = ClientAutoReconnect { logon_id: 1, security_verifier: EXPECTED_VERIFIER };
assert!(!auto_reconnect_cookie_matches(&cookie, &wrong_id));
}
}One minor API nit while I was here: Overall: LGTM and suitable — validated against a real client. Thanks for picking this up so fast! |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Greg Lamberson (@glamberson): addressed the tolerant client-cookie decoding, two-cookie rotation grace window, explicit revocation behavior, and validator documentation in 546f061. I am keeping #1496 and #1501 as the precedence path per the existing merge-order discussion. |
44f675e
into
master
The auto-reconnect cookie is credential material in both directions, and
both directions are logged today at debug level.
ServerAutoReconnect::random_bits keys the HMAC that proves, on reconnect,
that a client was the one last attached to the session (MS-RDPBCGR 5.5).
ClientAutoReconnect::security_verifier is that HMAC. Under Enhanced RDP
Security there is no client random, so 5.5 computes the verifier over 32
zero bytes: it is constant for a given cookie, and replaying it is enough
to resume the session. ExtendedClientOptionalInfo::reconnect_cookie is
the wire form of the same verifier.
All three derived Debug, and a client running at debug level writes both
halves to its log:
ironrdp-session/src/x224/mod.rs:212 logs the whole SaveSessionInfoPdu
it receives, so the random lands
in the log
ironrdp-connector/src/connection.rs:873 logs the whole ClientInfoPdu
it sends, so the verifier lands
in the log
That second line is why Credentials in this file already hand-writes
Debug to hide the password. The verifier travels in the same PDU and
deserves the same treatment.
This is not theoretical. A contributor validating Devolutions#1509 against mstsc
posted a server log reading
auto-reconnect cookie provisioned ... random_bits=[44, 11, 95, ...]
which is a live reconnect credential in a public comment, produced by the
derived impl.
Hand-write Debug on the three types following the Credentials pattern.
logon_id is not secret and stays visible, so the output remains useful
for diagnosis. Redacting the parsed field alone would not be enough,
since reconnect_cookie carries the same bytes unparsed, and redacting the
leaf alone would not be enough either, so the tests assert the elision
survives nesting inside the Debug-derived parents.
…onnect cookie A client that loses its connection ungracefully can reattach to the session instead of asking the user to log on again, provided it returns the cookie the server issued ([MS-RDPBCGR] 1.3.1.5). Devolutions#1509 built the server half of that: it validates a returning ARC_CS_PRIVATE_PACKET and rotates the random. The client half is still missing. The session layer decodes the cookie and drops it, and the connector has no way to send one back, so ironrdp-client cannot resume a session against ironrdp-server or against mstsc's peer. The wire encoding was already in place. ExtendedClientOptionalInfo carries, encodes and decodes a 28-byte autoReconnectCookie and its builder already had a reconnect_cookie step; ServerAutoReconnect already decoded, and Devolutions#1509 added the ClientAutoReconnect structure and its decode. Nothing connected them. Receive: SaveSessionInfo now surfaces the cookie as ProcessorOutput::AutoReconnectCookie and ActiveStageOutput::AutoReconnectCookie rather than logging it and returning nothing. The server replaces the cookie on every connect and again hourly ([MS-RDPBCGR] 3.3.6.2), so this can arrive more than once per session and the consumer keeps the most recent. Derive: ClientAutoReconnect gains from_server_cookie, implementing [MS-RDPBCGR] 5.5, SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom), keyed by the server's 16 random bytes with MD5 as the hash. Enhanced RDP Security generates no client random and 5.5 substitutes 32 zero bytes; IronRDP has no Standard RDP Security path, so that is the only case. This moves hmac into ironrdp-pdu, alongside the md-5 it already used. Send: ClientConnector::with_auto_reconnect_cookie takes the cookie last received and makes the connector put the derived Client Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. Absent, that PDU is byte-for-byte what it was. Unlike the server packet this structure has no enclosing logon-info field header, so it encodes to exactly the 28 bytes the cookie field expects. to_bytes writes that layout directly rather than through Encode, so filling a fixed-size field has no error path; a test pins the two to agree. One derivation, not two. Putting from_server_cookie in ironrdp-pdu would otherwise duplicate the private HMAC in ironrdp-server that Devolutions#1509 added, so this also gives ClientAutoReconnect a verify method and routes the server through it. verify keeps the constant-time comparison the server had: the verifier is the whole credential, so an early-exit compare would leak it a byte at a time. The server keeps only the policy around it, which cookies are live and whether the security protocol permits auto-reconnect, and drops its hmac and md-5 dependencies. That move also rehomes the known-answer tests @clintcan contributed on Devolutions#1509. They were written as an inline #[cfg(test)] module in ironrdp-server, which sets [lib] test = false, so they never ran. They now live in ironrdp-testsuite-core against the public API, where CI executes them, together with his HMAC-MD5 reference vector and the cases covering a tampered verifier and a mismatched logon ID. ironrdp-client, ironrdp-web and the FFI bindings gain an arm for the new output. None of them reconnect automatically yet, which is the remaining part of Devolutions#271. BREAKING CHANGE: ActiveStageOutput and x224::ProcessorOutput gain a variant and ClientConnector gains a public field, so exhaustive matches and struct literals need updating. Confirmed with cargo-semver-checks; the ironrdp-pdu half is additive.
## Summary - parse and carry `ARC_CS_PRIVATE_PACKET` data through the acceptor - validate returning Enhanced RDP Security cookies with HMAC-MD5 before reconnecting - rotate reconnect randoms per connection and hourly, with runtime cookie updates - restrict cookie authentication to TLS/Hybrid and document the behavior ## Testing - `cargo test -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server` - `cargo clippy -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server --all-targets -- -D warnings` Fixes: #1508 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…onnect cookie A client that loses its connection ungracefully can reattach to the session instead of asking the user to log on again, provided it returns the cookie the server issued ([MS-RDPBCGR] 1.3.1.5). Devolutions#1509 built the server half of that: it validates a returning ARC_CS_PRIVATE_PACKET and rotates the random. The client half is still missing. The session layer decodes the cookie and drops it, and the connector has no way to send one back, so ironrdp-client cannot resume a session against ironrdp-server or against mstsc's peer. The wire encoding was already in place. ExtendedClientOptionalInfo carries, encodes and decodes a 28-byte autoReconnectCookie and its builder already had a reconnect_cookie step; ServerAutoReconnect already decoded, and Devolutions#1509 added the ClientAutoReconnect structure and its decode. Nothing connected them. Receive: SaveSessionInfo now surfaces the cookie as ProcessorOutput::AutoReconnectCookie and ActiveStageOutput::AutoReconnectCookie rather than logging it and returning nothing. The server replaces the cookie on every connect and again hourly ([MS-RDPBCGR] 3.3.6.2), so this can arrive more than once per session and the consumer keeps the most recent. Derive: ClientAutoReconnect gains from_server_cookie, implementing [MS-RDPBCGR] 5.5, SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom), keyed by the server's 16 random bytes with MD5 as the hash. Enhanced RDP Security generates no client random and 5.5 substitutes 32 zero bytes; IronRDP has no Standard RDP Security path, so that is the only case. This moves hmac into ironrdp-pdu, alongside the md-5 it already used. Send: ClientConnector::with_auto_reconnect_cookie takes the cookie last received and makes the connector put the derived Client Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. Absent, that PDU is byte-for-byte what it was. Unlike the server packet this structure has no enclosing logon-info field header, so it encodes to exactly the 28 bytes the cookie field expects. to_bytes writes that layout directly rather than through Encode, so filling a fixed-size field has no error path; a test pins the two to agree. One derivation, not two. Putting from_server_cookie in ironrdp-pdu would otherwise duplicate the private HMAC in ironrdp-server that Devolutions#1509 added, so this also gives ClientAutoReconnect a verify method and routes the server through it. verify keeps the constant-time comparison the server had: the verifier is the whole credential, so an early-exit compare would leak it a byte at a time. The server keeps only the policy around it, which cookies are live and whether the security protocol permits auto-reconnect, and drops its hmac and md-5 dependencies. That move also rehomes the known-answer tests @clintcan contributed on Devolutions#1509. They were written as an inline #[cfg(test)] module in ironrdp-server, which sets [lib] test = false, so they never ran. They now live in ironrdp-testsuite-core against the public API, where CI executes them, together with his HMAC-MD5 reference vector and the cases covering a tampered verifier and a mismatched logon ID. ironrdp-client, ironrdp-web and the FFI bindings gain an arm for the new output. None of them reconnect automatically yet, which is the remaining part of Devolutions#271. BREAKING CHANGE: ActiveStageOutput and x224::ProcessorOutput gain a variant and ClientConnector gains a public field, so exhaustive matches and struct literals need updating. Confirmed with cargo-semver-checks; the ironrdp-pdu half is additive.
…uto-reconnect cookie (#1501) > **Rebased onto post-#1522 master.** #1509 landed the server half of #1508 while this was open, including the `ClientAutoReconnect` structure. This PR no longer declares it; it extends it, and picks up the parts #1509 did not build. ## What The client half of automatic reconnection. The session layer surfaces the Server Auto-Reconnect Cookie, `ironrdp-pdu` derives and verifies the client's response to it, and the connector sends that response when resuming a session. ## Why A client whose connection drops ungracefully can reattach to its session instead of making the user log on again, provided it returns the cookie the server issued during logon ([MS-RDPBCGR] 1.3.1.5). #1509 built the server side of that: it validates a returning `ARC_CS_PRIVATE_PACKET` and rotates the random. Nothing answers it. `ironrdp-session` decodes the cookie and drops it, `ironrdp-connector` has no way to send one back, and `TODO(#271)` still sits in `ironrdp-client`. So `ironrdp-client` cannot resume a session against `ironrdp-server`, and the validation #1509 added has no in-tree counterpart to exercise it. The wire encoding was already there. `ExtendedClientOptionalInfo` carries, encodes and decodes a 28-byte `autoReconnectCookie` and its builder already had a `reconnect_cookie` step; `ServerAutoReconnect` already decoded; #1509 added `ClientAutoReconnect` and its decode. Nothing connected them. ## The three parts **Receive.** `SaveSessionInfo` now also surfaces the cookie, as `ProcessorOutput::AutoReconnectCookie` and `ActiveStageOutput::AutoReconnectCookie`. #1522 added a `SaveSessionInfo { logon_complete }` output on that same handler; the two coexist rather than compete, since both are read off one PDU and neither supersedes the other. The handler emits the logon notification unconditionally and appends the cookie when one is present, and a test pins that surfacing the cookie does not suppress the notification. #1509's server replaces the cookie whenever a client connects and again hourly ([MS-RDPBCGR] 3.3.6.2), so this can arrive more than once in a session and the consumer keeps the most recent. **Derive.** `ClientAutoReconnect::from_server_cookie` implements [MS-RDPBCGR] 5.5: > The auto-reconnect random is used to key the HMAC function ([RFC2104]), which uses MD5 as the iterative hash function. The security verifier is derived by applying the HMAC to the client random received in Step 3. > > `SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom)` > > When Enhanced RDP Security is in effect the client random value is not generated (section 5.3.2). In this case, for the purpose of generating the security verifier, the client random is assumed to be an array of 32 zero bytes. IronRDP implements no Standard RDP Security path (there is no Security Exchange PDU), so the zero-client-random case is the only one that arises. As 5.5 notes, that makes the verifier constant for a given cookie, so it proves possession of the cookie and nothing more; session security comes from the outer TLS/CredSSP handshake. @clintcan independently confirmed this construction against real **mstsc** while validating #1509 ([comment](#1509 (comment))): a Windows client's `ARC_CS_PRIVATE_PACKET` verifies against `HMAC-MD5(random_bits, [0u8; 32])`. That is the same derivation implemented here, so the two halves interoperate with Microsoft's client and not only with each other. **Send.** `ClientConnector::with_auto_reconnect_cookie` takes the cookie last received and makes the connector put the derived Client Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. Absent, that PDU is byte-for-byte what it was. Unlike the server packet, this structure has no enclosing logon-info field header, so it encodes to exactly the 28 bytes the cookie field expects. `to_bytes` writes that layout directly rather than going through `Encode`, so filling a fixed-size field has no error path a caller must handle; a test pins the two to agree. ## One derivation, not two Putting `from_server_cookie` in `ironrdp-pdu` would leave the workspace with two implementations of 5.5, since #1509 added a private HMAC to `ironrdp-server`. So `ClientAutoReconnect` also gains `verify`, and the server routes through it. `verify` keeps the constant-time comparison the server had. The verifier is the whole credential, so a comparison returning early on the first differing byte would let a peer recover it a byte at a time from the timing; the session identifier is not secret and is compared normally. `ironrdp-server` keeps the policy around the check, which cookies are live and whether the security protocol permits auto-reconnect, and drops its `hmac` and `md-5` dependencies. `hmac` moves to `ironrdp-pdu` as `default-features = false`; the crate's full feature powerset still checks clean, including `--no-default-features`. I would rather not have reached into `ironrdp-server` in a `pdu,session,connector` change, but the alternative was shipping the duplicate and filing a follow-up to remove it, which is a worse trade for reviewer time. ## Tests that were not running That move also rehomes the known-answer tests @clintcan contributed on #1509. They went in as an inline `#[cfg(test)]` module in `crates/ironrdp-server/src/server.rs`, and that crate sets `[lib] test = false`, so they have never executed in CI. They now live in `ironrdp-testsuite-core` against the public API, where CI runs them: his HMAC-MD5 reference vector is kept as a second vector alongside a differently-keyed one, plus the cases for a tampered verifier and a mismatched logon ID. Worth flagging separately: `ironrdp-server` is not alone. `ironrdp-agent`, `ironrdp-session` and `ironrdp-web` also set `[lib] test = false` and between them carry 16 files of inline `#[cfg(test)]` modules that CI never runs. That is out of scope here, but I am happy to open an issue if it would be useful. ## Breaking changes `ActiveStageOutput` and `x224::ProcessorOutput` gain a variant, and `ClientConnector` gains a public field, so exhaustive matches and struct literals need updating. Confirmed with `cargo-semver-checks` against the merge-base: those three are the only findings this branch introduces. The others it reports on `master` today (`ShareDataPdu::Compressed` and the `ShareDataCtx` fields from #1518, `ProcessorBuilder.bulk_decompressor` from #1518, `ServerEvent::SetAutoReconnectCookie` from #1509) are present on `master` unchanged. The `ironrdp-pdu` additions are additive. ## Scope This is the library half. `ironrdp-client`, `ironrdp-web` and the FFI bindings gain an arm for the new output but none of them reconnect automatically yet; that is the remaining part of #271, and the existing `TODO(#271)` in `ironrdp-client` marks where it goes. I kept receive, derive and send together deliberately. Split up, none of them is usable on its own: without the receive half there is no way to obtain a cookie, and without the send half there is nothing to do with one. ## Tests Thirteen, all in `ironrdp-testsuite-core`. On the packet and the derivation: the `SecurityVerifier` matches two independently computed HMAC-MD5 vectors of 32 zero bytes under different keys, so the tests pin the derivation rather than restating the code; the logon ID carries over from the server cookie; the encoding matches the 2.2.4.3 field layout byte for byte with `cbLen` fixed at `0x1C`; `to_bytes` agrees with `Encode`; it round-trips; and it rejects both a wrong packet length and an unknown version. On verification: a derived answer is accepted, a single flipped byte in the verifier is rejected, a correct verifier under a different logon ID is rejected, and an answer derived from a different random is rejected. On the surfacing path: a Save Session Info PDU framed the way a server sends it, through the real x224 processor, yields an `AutoReconnectCookie` carrying the right logon ID and random bits, alongside #1522's logon notification rather than in place of it; and one without a cookie surfaces no cookie. ## Verification `cargo xtask check fmt/lints/tests/typos/locks` all pass on 1.94.1, including a `fuzz/` build before the lock check. ## Note #1496 also touches the `ClientAutoReconnect` declaration. Whichever of the two lands second needs a one-line rebase on the derive attribute; happy to take that in either order. [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/e729948a-3f4e-4568-9aef-d355e30b5389 [RFC2104]: https://www.rfc-editor.org/rfc/rfc2104
The auto-reconnect cookie is credential material in both directions, and
both directions are logged today at debug level.
ServerAutoReconnect::random_bits keys the HMAC that proves, on reconnect,
that a client was the one last attached to the session (MS-RDPBCGR 5.5).
ClientAutoReconnect::security_verifier is that HMAC. Under Enhanced RDP
Security there is no client random, so 5.5 computes the verifier over 32
zero bytes: it is constant for a given cookie, and replaying it is enough
to resume the session. ExtendedClientOptionalInfo::reconnect_cookie is
the wire form of the same verifier.
All three derived Debug, and a client running at debug level writes both
halves to its log:
ironrdp-session/src/x224/mod.rs:212 logs the whole SaveSessionInfoPdu
it receives, so the random lands
in the log
ironrdp-connector/src/connection.rs:873 logs the whole ClientInfoPdu
it sends, so the verifier lands
in the log
That second line is why Credentials in this file already hand-writes
Debug to hide the password. The verifier travels in the same PDU and
deserves the same treatment.
This is not theoretical. A contributor validating Devolutions#1509 against mstsc
posted a server log reading
auto-reconnect cookie provisioned ... random_bits=[44, 11, 95, ...]
which is a live reconnect credential in a public comment, produced by the
derived impl.
Hand-write Debug on the three types following the Credentials pattern.
logon_id is not secret and stays visible, so the output remains useful
for diagnosis. Redacting the parsed field alone would not be enough,
since reconnect_cookie carries the same bytes unparsed, and redacting the
leaf alone would not be enough either, so the tests assert the elision
survives nesting inside the Debug-derived parents.
Summary
ARC_CS_PRIVATE_PACKETdata through the acceptorTesting
cargo test -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-servercargo clippy -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server --all-targets -- -D warningsFixes: #1508