diff --git a/Cargo.lock b/Cargo.lock index 9f38adae40..50e1866dea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3604,7 +3604,6 @@ dependencies = [ "qoicoubeh", "rand 0.9.5", "rayon", - "rustls-pemfile", "tokio", "tokio-rustls", "tracing", @@ -6106,15 +6105,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.15.1" diff --git a/crates/ironrdp-acceptor/src/connection.rs b/crates/ironrdp-acceptor/src/connection.rs index 850da4d6a1..35435fef38 100644 --- a/crates/ironrdp-acceptor/src/connection.rs +++ b/crates/ironrdp-acceptor/src/connection.rs @@ -143,13 +143,16 @@ pub struct Acceptor { client_cluster_data: Option, password_cookie: Option, client_info_flags: Option, + alternate_shell: String, + work_dir: String, performance_flags: PerformanceFlags, remote_app_capabilities: Option, server_capabilities: Vec, static_channels: StaticChannelSet, pending_input_events: InputEventBuffer, saved_for_reactivation: AcceptorState, - pub(crate) creds: Option, + /// Accepted credentials; empty accepts any (ClientInfo) and fails CredSSP. + pub(crate) creds: Vec, pub(crate) credssp_backend: crate::CredsspBackend, received_credentials: Option, credentials_handled: bool, @@ -458,6 +461,13 @@ pub struct AcceptorResult { /// /// [2.2.1.11.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/732394f5-e2b5-4ac5-8a0a-35345386b0d1 pub client_info_flags: ClientInfoFlags, + /// The program the client asked the session to run instead of the shell, + /// from its Client Info PDU ([MS-RDPBCGR] 2.2.1.11.1.1, `AlternateShell`). + /// Empty when it asked for none. A server without such programs may read + /// it as a client preference. + pub alternate_shell: String, + /// The working directory for [`Self::alternate_shell`] (`WorkingDir`); empty when none. + pub work_dir: String, } impl Acceptor { @@ -488,13 +498,15 @@ impl Acceptor { client_cluster_data: None, password_cookie: None, client_info_flags: None, + alternate_shell: String::new(), + work_dir: String::new(), performance_flags: PerformanceFlags::empty(), remote_app_capabilities: None, server_capabilities: capabilities, static_channels: StaticChannelSet::new(), pending_input_events: InputEventBuffer::default(), saved_for_reactivation: Default::default(), - creds, + creds: creds.into_iter().collect(), credssp_backend: crate::CredsspBackend::default(), received_credentials: None, credentials_handled: false, @@ -561,6 +573,15 @@ impl Acceptor { self.honor_client_desktop_size = max; } + /// Accept any of `creds`, replacing the credentials given to [`Self::new`]. + /// + /// CredSSP checks the client's password against the entry with its user + /// name; ClientInfo credentials must match one of them. An empty list + /// accepts any ClientInfo credentials and fails CredSSP, as `None` does. + pub fn set_credential_list(&mut self, creds: Vec) { + self.creds = creds; + } + /// Selects the CredSSP authentication provider before negotiation begins. pub fn set_credssp_backend(&mut self, backend: crate::CredsspBackend) { self.credssp_backend = backend; @@ -748,6 +769,8 @@ impl Acceptor { client_cluster_data: consumed.client_cluster_data, password_cookie: consumed.password_cookie, client_info_flags: consumed.client_info_flags, + alternate_shell: consumed.alternate_shell, + work_dir: consumed.work_dir, performance_flags: consumed.performance_flags, remote_app_capabilities: None, server_capabilities: consumed.server_capabilities, @@ -1003,6 +1026,8 @@ impl Acceptor { client_cluster_data: self.client_cluster_data.clone(), password_cookie: self.password_cookie.clone(), client_info_flags: self.client_info_flags.unwrap_or_else(ClientInfoFlags::empty), + alternate_shell: self.alternate_shell.clone(), + work_dir: self.work_dir.clone(), }), previous_state => { self.state = previous_state; @@ -1509,6 +1534,9 @@ impl Sequence for Acceptor { let client_info: rdp::ClientInfoPdu = decode(data.user_data.as_ref()).map_err(ConnectorError::decode)?; self.client_info_flags = Some(client_info.client_info.flags); + self.alternate_shell + .clone_from(&client_info.client_info.alternate_shell); + self.work_dir.clone_from(&client_info.client_info.work_dir); self.performance_flags = client_info .client_info .extra_info @@ -1534,8 +1562,8 @@ impl Sequence for Acceptor { let creds = client_info.client_info.credentials; if !self.allow_unverified_credentials - && let Some(expected) = &self.creds - && !credentials_match(expected, &creds) + && !self.creds.is_empty() + && !self.creds.iter().any(|expected| credentials_match(expected, &creds)) { self.encode_access_denied(output)?; return Err(ConnectorError::general("invalid credentials")); diff --git a/crates/ironrdp-acceptor/src/credssp.rs b/crates/ironrdp-acceptor/src/credssp.rs index 7de3ab0d48..f1dc7c16ef 100644 --- a/crates/ironrdp-acceptor/src/credssp.rs +++ b/crates/ironrdp-acceptor/src/credssp.rs @@ -80,11 +80,11 @@ pub struct CredsspSequence<'a> { #[derive(Debug)] struct CredentialsProxyImpl<'a> { - credentials: &'a AuthIdentity, + credentials: &'a [AuthIdentity], } impl<'a> CredentialsProxyImpl<'a> { - fn new(credentials: &'a AuthIdentity) -> Self { + fn new(credentials: &'a [AuthIdentity]) -> Self { Self { credentials } } } @@ -93,18 +93,22 @@ impl CredentialsProxy for CredentialsProxyImpl<'_> { type AuthenticationData = AuthIdentity; fn auth_data_by_user(&mut self, username: &Username) -> std::io::Result { - if !usernames_match(&self.credentials.username, username) { + let Some(credentials) = self + .credentials + .iter() + .find(|credentials| usernames_match(&credentials.username, username)) + else { return Err(std::io::Error::other("invalid username")); - } + }; - let mut data = self.credentials.clone(); + let mut data = credentials.clone(); // keep the original user/domain data.username = username.clone(); Ok(data) } fn auth_data(&mut self) -> Result, std::io::Error> { - Ok(vec![self.credentials.clone()]) + Ok(self.credentials.to_vec()) } } @@ -222,6 +226,22 @@ impl<'a> CredsspSequence<'a> { client_computer_name: ServerName, public_key: Vec, krb_config: Option, + ) -> ConnectorResult { + Self::init_with_identities( + core::slice::from_ref(creds), + client_computer_name, + public_key, + krb_config, + ) + } + + /// Like [`Self::init`], accepting any of several identities: the client's + /// user name picks the one its password is checked against. + pub fn init_with_identities( + creds: &'a [AuthIdentity], + client_computer_name: ServerName, + public_key: Vec, + krb_config: Option, ) -> ConnectorResult { let client_computer_name = client_computer_name.into_inner(); let new_server = |server_mode| { diff --git a/crates/ironrdp-acceptor/src/lib.rs b/crates/ironrdp-acceptor/src/lib.rs index 4387469a0c..9476345a1c 100644 --- a/crates/ironrdp-acceptor/src/lib.rs +++ b/crates/ironrdp-acceptor/src/lib.rs @@ -483,23 +483,32 @@ where } } - let creds = acceptor - .creds - .as_ref() - .ok_or_else(|| general_err!("no credentials while doing credssp"))?; - let username = if creds.domain.is_none() && creds.username.contains(['\\', '@']) { - Username::parse(&creds.username) - } else { - Username::new(&creds.username, creds.domain.as_deref()) + if acceptor.creds.is_empty() { + return Err(general_err!("no credentials while doing credssp")); } - .map_err(|e| custom_err!("invalid username", e))?; - let identity = AuthIdentity { - username, - password: creds.password.clone().into(), - }; - - let mut sequence = - credssp::CredsspSequence::init(&identity, client_computer_name, public_key, kerberos_config)?; + let identities = acceptor + .creds + .iter() + .map(|creds| { + let username = if creds.domain.is_none() && creds.username.contains(['\\', '@']) { + Username::parse(&creds.username) + } else { + Username::new(&creds.username, creds.domain.as_deref()) + } + .map_err(|e| custom_err!("invalid username", e))?; + Ok(AuthIdentity { + username, + password: creds.password.clone().into(), + }) + }) + .collect::>>()?; + + let mut sequence = credssp::CredsspSequence::init_with_identities( + &identities, + client_computer_name, + public_key, + kerberos_config, + )?; loop { let Some(next_pdu_hint) = sequence.next_pdu_hint()? else { diff --git a/crates/ironrdp-rdpdr/src/pdu/efs.rs b/crates/ironrdp-rdpdr/src/pdu/efs.rs index a84caf6592..bbd491fe77 100644 --- a/crates/ironrdp-rdpdr/src/pdu/efs.rs +++ b/crates/ironrdp-rdpdr/src/pdu/efs.rs @@ -525,6 +525,16 @@ impl CapabilityMessage { } } + /// Whether this is a general capability set whose `extendedPDU` has + /// `RDPDR_USER_LOGGEDON_PDU`: the client takes the Server User Logged On PDU + /// ([MS-RDPEFS] 2.2.2.7.1). + pub fn user_logged_on_supported(&self) -> bool { + match &self.capability_data { + CapabilityData::General(general) => general.extended_pdu.contains(ExtendedPdu::RDPDR_USER_LOGGEDON_PDU), + _ => false, + } + } + /// Creates a new [`SMARTCARD_CAPS_SET`]. /// /// [`SMARTCARD_CAPS_SET`]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e02de60a-4d32-4dc7-ab17-9d591129eb93 @@ -1525,7 +1535,7 @@ impl DeviceAnnounceHeader { self.preferred_dos_name.0.strip_prefix("PRN")?.parse().ok() } - pub(crate) fn device_type(&self) -> DeviceType { + pub fn device_type(&self) -> DeviceType { self.device_type } @@ -5593,9 +5603,9 @@ impl FileAllocationInformation { /// [2.2.3.4.9]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/16b893d5-5d8b-49d1-8dcb-ee21e7612970 #[derive(Debug, PartialEq, Clone)] pub struct ClientDriveSetInformationResponse { - pub(crate) device_io_reply: DeviceIoResponse, + pub device_io_reply: DeviceIoResponse, /// This field MUST be equal to the Length field in the Server Drive Set Information Request (section 2.2.3.3.9). - pub(crate) length: u32, + pub length: u32, } impl ClientDriveSetInformationResponse { diff --git a/crates/ironrdp-rdpdr/src/server.rs b/crates/ironrdp-rdpdr/src/server.rs index 6f276a61c1..92948cf9f5 100644 --- a/crates/ironrdp-rdpdr/src/server.rs +++ b/crates/ironrdp-rdpdr/src/server.rs @@ -545,7 +545,20 @@ impl RdpdrServer { "received ClientCoreCapabilityResponse" ); self.state = RdpdrServerState::Active; - Ok(Vec::new()) + // [MS-RDPEFS] 3.3.5.1.5: the server sends Server User Logged On once the + // user has logged on, to a client whose general capability set has + // RDPDR_USER_LOGGEDON_PDU; clients announce their drives only after it + // (3.1.5.1.5). The user was authenticated with the connection, so the + // PDU follows the capability exchange at once. + if response + .capabilities + .iter() + .any(CapabilityMessage::user_logged_on_supported) + { + Ok(vec![SvcMessage::from(RdpdrPdu::UserLoggedon)]) + } else { + Ok(Vec::new()) + } } fn handle_device_list_announce(&mut self, src: &mut ReadCursor<'_>) -> PduResult> { diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml index 46a22fe5cd..a4a1bcce98 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -18,7 +18,7 @@ test = false [features] default = ["rayon", "qoi", "qoiz"] -helper = ["dep:x509-cert", "dep:rustls-pemfile"] +helper = ["dep:x509-cert"] rayon = ["dep:rayon"] qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] qoiz = ["dep:zstd-safe", "qoi", "ironrdp-pdu/qoiz"] @@ -66,7 +66,6 @@ ironrdp-rdpeudp = { path = "../ironrdp-rdpeudp", version = "0.1" } # public ironrdp-rdpeudp-tokio = { path = "../ironrdp-rdpeudp-tokio", version = "0.2" } # public tracing = { version = "0.1", features = ["log"] } x509-cert = { version = "0.3", optional = true } -rustls-pemfile = { version = "2.2", optional = true } rayon = { version = "1.12", optional = true } bytes = "1" visibility = { version = "0.1", optional = true } diff --git a/crates/ironrdp-server/src/handler.rs b/crates/ironrdp-server/src/handler.rs index 2978f44c34..bda39f18f1 100644 --- a/crates/ironrdp-server/src/handler.rs +++ b/crates/ironrdp-server/src/handler.rs @@ -111,6 +111,11 @@ pub enum MouseButton { pub trait RdpServerInputHandler: Send { fn keyboard(&mut self, event: KeyboardEvent); fn mouse(&mut self, event: MouseEvent); + + /// The client opened the advanced input channel (`FreeRDP::Advanced::Input`, + /// [`ironrdp_ainput`]), which FreeRDP-based clients have and the Microsoft + /// clients do not. Its mouse events arrive through [`Self::mouse`]. + fn advanced_input_opened(&mut self) {} } impl From<(u8, fast_path::KeyboardFlags)> for KeyboardEvent { diff --git a/crates/ironrdp-server/src/helper.rs b/crates/ironrdp-server/src/helper.rs index d4b52b3246..7b650dd8e2 100644 --- a/crates/ironrdp-server/src/helper.rs +++ b/crates/ironrdp-server/src/helper.rs @@ -3,9 +3,8 @@ use std::io::BufReader; use std::path::Path; use std::sync::Arc; -use rustls_pemfile::{certs, pkcs8_private_keys}; use tokio_rustls::rustls::pki_types::pem::PemObject as _; -use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use tokio_rustls::{TlsAcceptor, rustls}; use crate::error::{ServerError, ServerErrorExt as _, ServerResult}; @@ -27,23 +26,23 @@ impl TlsIdentityCtx { .collect::, _>>() .map_err(|e| ServerError::custom("collecting server cert", e))? } else { - certs(&mut BufReader::new( + CertificateDer::pem_reader_iter(BufReader::new( File::open(cert_path).map_err(|e| ServerError::io("opening server cert", e))?, )) .collect::, _>>() - .map_err(|e| ServerError::io("collecting server cert", e))? + .map_err(|e| ServerError::custom("collecting server cert", e))? }; let priv_key = if key_path.extension().is_some_and(|ext| ext == "pem") { PrivateKeyDer::from_pem_file(key_path).map_err(|e| ServerError::custom("reading server key", e))? } else { - pkcs8_private_keys(&mut BufReader::new( + PrivatePkcs8KeyDer::pem_reader_iter(BufReader::new( File::open(key_path).map_err(|e| ServerError::io("opening server key", e))?, )) .next() .ok_or_else(|| ServerError::reason("server key", "no private key"))? .map(PrivateKeyDer::from) - .map_err(|e| ServerError::io("reading server key", e))? + .map_err(|e| ServerError::custom("reading server key", e))? }; let pub_key = { diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index e153c8a487..7b55207f19 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -239,6 +239,10 @@ pub struct ConnectionInfo { /// The server itself doesn't start the rdpsnd channel when they ask for no audio redirection; the rest, such as /// whether to offer a microphone, is the embedder's to act on. pub client_info_flags: rdp::client_info::ClientInfoFlags, + /// See [`ironrdp_acceptor::AcceptorResult::alternate_shell`]. + pub alternate_shell: String, + /// See [`ironrdp_acceptor::AcceptorResult::work_dir`]. + pub work_dir: String, /// The remote address of this connection, as its [`IncomingConnection::peer`] gave it: the address that /// [`ConnectionHandler::on_accept`] and [`ConnectionHandler::on_disconnected`] get for this connection. Under /// [`ConnectionPolicy::Preempt`], `on_accept` also runs for newcomers while a session is live, so the peer of the @@ -268,6 +272,8 @@ impl ConnectionInfo { redirected_session_id: None, password_cookie: None, client_info_flags: rdp::client_info::ClientInfoFlags::empty(), + alternate_shell: String::new(), + work_dir: String::new(), peer_addr: None, } } @@ -635,6 +641,16 @@ impl RdpServerInputHandler for BoundInputSlot { self.default.mouse(event); } } + + fn advanced_input_opened(&mut self) { + let mut bound = self.bound.lock().expect("bound input lock poisoned"); + if let Some(handler) = bound.as_mut() { + handler.advanced_input_opened(); + } else { + drop(bound); + self.default.advanced_input_opened(); + } + } } /// A built-in [`CredentialValidator`] that accepts exactly one fixed set of credentials. @@ -976,6 +992,11 @@ impl dvc::DvcProcessor for AInputHandler { fn start(&mut self, _channel_id: u32) -> PduResult> { use ironrdp_ainput::{ServerPdu, VersionPdu}; + let handler = Arc::clone(&self.handler); + task::spawn_blocking(move || { + handler.blocking_lock().advanced_input_opened(); + }); + let pdu = ServerPdu::Version(VersionPdu::default()); Ok(vec![Box::new(pdu)]) @@ -1353,7 +1374,7 @@ pub struct RdpServer { handle: RdpServerHandle, ev_sender: mpsc::UnboundedSender, ev_receiver: Arc>>, - creds: Option, + creds: Vec, credential_validator: Option>, connection_binder: Option>, pending_authenticated_credentials: Arc>>, @@ -1635,6 +1656,9 @@ pub enum ServerEvent { Rdpeai(RdpeaiServerMessage), Echo(EchoServerMessage), SetCredentials(Credentials), + /// Accept any of these credentials from the next connection on; see + /// [`RdpServer::set_credential_list`]. + SetCredentialList(Vec), /// Replace or clear the Server Auto-Reconnect Cookie. SetAutoReconnectCookie(Option), GetLocalAddr(oneshot::Sender>), @@ -1684,6 +1708,7 @@ impl fmt::Debug for ServerEvent { Self::Rdpeai(..) => f.write_str("Rdpeai(..)"), Self::Echo(..) => f.write_str("Echo(..)"), Self::SetCredentials(..) => f.write_str("SetCredentials(..)"), + Self::SetCredentialList(..) => f.write_str("SetCredentialList(..)"), Self::SetAutoReconnectCookie(Some(..)) => f.write_str("SetAutoReconnectCookie(Some(..))"), Self::SetAutoReconnectCookie(None) => f.write_str("SetAutoReconnectCookie(None)"), Self::GetLocalAddr(..) => f.write_str("GetLocalAddr(..)"), @@ -1732,6 +1757,16 @@ impl RdpServerHandle { .map_err(|_| ServerError::channel("send server credentials event")) } + /// Queues a list of accepted credentials for subsequent incoming + /// connections; see [`RdpServer::set_credential_list`]. + /// + /// Success confirms queuing, not that the credentials have changed. + pub fn set_credential_list(&self, credentials: Vec) -> ServerResult<()> { + self.sender + .send(ServerEvent::SetCredentialList(credentials)) + .map_err(|_| ServerError::channel("send server credentials event")) + } + /// Queries the listener address through the server event loop. /// /// Returns the address bound by [`RdpServer::run`], or `None` if it has never bound a listener. @@ -1876,12 +1911,13 @@ impl PendingConnection { security: RdpServerSecurity, desktop_size: DesktopSize, capabilities: Vec, - creds: Option, + creds: Vec, honor_client_desktop_size: Option, udp_bind_addr: Option, credentials_handler: ServerCredentialsHandler, ) -> Self { - let mut acceptor = Acceptor::new(security.flag(), desktop_size, capabilities, creds); + let mut acceptor = Acceptor::new(security.flag(), desktop_size, capabilities, None); + acceptor.set_credential_list(creds); acceptor.set_honor_client_desktop_size(honor_client_desktop_size); // Multitransport requires Enhanced Security: `RdpServerSecurity::None` // has nothing to authenticate the sideband transport's TLS with, and @@ -2316,7 +2352,7 @@ fn refuse_reconnect_from_evicted( /// [`RdpServer::serve_negotiated`], from `self`. struct NegotiationContext { opts: RdpServerOptions, - creds: Option, + creds: Vec, display: Arc>>, credential_validator: Option>, requires_authenticated_credentials: bool, @@ -2508,7 +2544,7 @@ impl RdpServer { }, ev_sender, ev_receiver: Arc::new(Mutex::new(ev_receiver)), - creds: None, + creds: Vec::new(), credential_validator: None, connection_binder: None, pending_authenticated_credentials: Arc::new(StdMutex::new(None)), @@ -3140,6 +3176,7 @@ impl RdpServer { ServerEvent::Quit(_) | ServerEvent::GetLocalAddr(_) | ServerEvent::SetCredentials(_) + | ServerEvent::SetCredentialList(_) | ServerEvent::SetAutoReconnectCookie(None) => true, // On request, a cookie is a vouch that the connection that was // current when it was sent has authenticated its user. That @@ -3537,6 +3574,9 @@ impl RdpServer { ServerEvent::SetCredentials(creds) => { self.set_credentials(Some(creds)); } + ServerEvent::SetCredentialList(creds) => { + self.set_credential_list(creds); + } ServerEvent::SetAutoReconnectCookie(cookie) => { self.set_auto_reconnect_cookie(cookie); } @@ -4239,6 +4279,9 @@ impl RdpServer { ServerEvent::SetCredentials(creds) => { self.set_credentials(Some(creds)); } + ServerEvent::SetCredentialList(creds) => { + self.set_credential_list(creds); + } ServerEvent::SetAutoReconnectCookie(cookie) => { self.update_auto_reconnect_cookie(conn, cookie, writer, io_channel_id, user_channel_id) .await?; @@ -5481,6 +5524,8 @@ impl RdpServer { .map(|cluster| cluster.redirected_session_id), password_cookie: result.password_cookie.clone(), client_info_flags: result.client_info_flags, + alternate_shell: result.alternate_shell.clone(), + work_dir: result.work_dir.clone(), peer_addr: self.connection_peer_addr, }); }); @@ -6242,7 +6287,16 @@ impl RdpServer { pub fn set_credentials(&mut self, creds: Option) { debug!(?creds, "Changing credentials"); - self.creds = creds + self.creds = creds.into_iter().collect(); + } + + /// Accept any of `creds` from the next connection on: CredSSP checks the + /// client's password against the entry with its user name, and + /// ClientInfo credentials must match one of them. A running session is + /// not affected. + pub fn set_credential_list(&mut self, creds: Vec) { + debug!(count = creds.len(), "Changing credentials"); + self.creds = creds; } /// Tell the server which local address the next client reached, for @@ -6733,7 +6787,7 @@ mod preempt_tests { remotefx_entropy_coder: None, udp_bind_addr: None, }, - creds: None, + creds: Vec::new(), display: Arc::new(Mutex::new(Box::new(NoDisplay))), credential_validator: None, requires_authenticated_credentials: false, diff --git a/crates/ironrdp-testsuite-core/tests/rdpdr/mod.rs b/crates/ironrdp-testsuite-core/tests/rdpdr/mod.rs index b7b640727b..6583bb58d2 100644 --- a/crates/ironrdp-testsuite-core/tests/rdpdr/mod.rs +++ b/crates/ironrdp-testsuite-core/tests/rdpdr/mod.rs @@ -2142,11 +2142,48 @@ impl RdpdrServerBackend for TrackingServerBackend { } } +/// A client whose capabilities lack a general set with RDPDR_USER_LOGGEDON_PDU is not sent the +/// Server User Logged On PDU. +// oracle: spec MS-RDPEFS §3.3.5.1.5 +#[test] +fn user_logged_on_goes_only_to_a_client_that_advertises_it() { + let mut server = RdpdrServer::new(Box::new(NoopRdpdrServerBackend)); + handshake_to_capability_exchange(&mut server); + + let capability_response = encode_vec(&RdpdrPdu::CoreCapability(CoreCapability { + capabilities: vec![CapabilityMessage::new_drive()], + kind: CoreCapabilityKind::ClientCoreCapabilityResponse, + })) + .unwrap(); + assert!(server.process(&capability_response).unwrap().is_empty()); +} + /// Drives a fresh [`RdpdrServer`] through the full MS-RDPEFS initialization handshake /// (`start()` through `ClientCoreCapabilityResponse`), leaving it in the `Active` state. /// Returns the `client_id` the server generated so callers can build further /// server-perspective client replies if needed. fn handshake_to_active(server: &mut RdpdrServer) -> u32 { + let client_id = handshake_to_capability_exchange(server); + + let capability_response = encode_vec(&RdpdrPdu::CoreCapability(CoreCapability { + capabilities: vec![CapabilityMessage::new_general(0), CapabilityMessage::new_drive()], + kind: CoreCapabilityKind::ClientCoreCapabilityResponse, + })) + .unwrap(); + // oracle: spec MS-RDPEFS §3.3.5.1.5 (the client's general capability set has RDPDR_USER_LOGGEDON_PDU) + let logged_on = server.process(&capability_response).unwrap(); + assert_eq!(logged_on.len(), 1); + assert_eq!( + logged_on[0].encode_unframed_pdu().unwrap(), + encode_vec(&RdpdrPdu::UserLoggedon).unwrap() + ); + + client_id +} + +/// Drives `server` through the RDPDR handshake up to the server's capability request, +/// and returns the `client_id` it generated. +fn handshake_to_capability_exchange(server: &mut RdpdrServer) -> u32 { let started = server.start().unwrap(); assert_eq!(started.len(), 1); let client_id = read_u32(&started[0].encode_unframed_pdu().unwrap()[4..]); @@ -2168,13 +2205,6 @@ fn handshake_to_active(server: &mut RdpdrServer) -> u32 { let capability_and_confirm = server.process(&name_request).unwrap(); assert_eq!(capability_and_confirm.len(), 2); - let capability_response = encode_vec(&RdpdrPdu::CoreCapability(CoreCapability { - capabilities: vec![CapabilityMessage::new_general(0), CapabilityMessage::new_drive()], - kind: CoreCapabilityKind::ClientCoreCapabilityResponse, - })) - .unwrap(); - assert!(server.process(&capability_response).unwrap().is_empty()); - client_id } diff --git a/crates/ironrdp-testsuite-core/tests/server/acceptor.rs b/crates/ironrdp-testsuite-core/tests/server/acceptor.rs index 182650734d..dbea12f823 100644 --- a/crates/ironrdp-testsuite-core/tests/server/acceptor.rs +++ b/crates/ironrdp-testsuite-core/tests/server/acceptor.rs @@ -1259,6 +1259,37 @@ fn client_info_flags_survive_reactivation() { assert_eq!(result.client_info_flags, flags); } +/// The `AlternateShell` and `WorkingDir` of the client's Info Packet, MS-RDPBCGR [2.2.1.11.1.1], reach the +/// `AcceptorResult` as sent, and survive a reactivation like the flags. +/// +/// [2.2.1.11.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/732394f5-e2b5-4ac5-8a0a-35345386b0d1 +// oracle: spec MS-RDPBCGR §2.2.1.11.1.1 +#[test] +fn alternate_shell_and_work_dir_are_kept_as_sent() { + let mut client_info = CLIENT_INFO_UNICODE.clone(); + client_info.alternate_shell = "curtain".to_owned(); + client_info.work_dir = "C:\\work".to_owned(); + let client_info = encode_vec(&ClientInfoPdu { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::INFO_PKT, + }, + client_info, + }) + .unwrap(); + + let mut client_blocks = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + client_blocks.network = None; + let (acceptor, result, user_channel_id, io_channel_id) = accepted(client_blocks, &client_info); + assert_eq!(result.alternate_shell, "curtain"); + assert_eq!(result.work_dir, "C:\\work"); + + let mut acceptor = Acceptor::new_deactivation_reactivation(acceptor, result.static_channels, DESKTOP_SIZE) + .expect("rebuild the acceptor for reactivation"); + let result = result_after_activation(&mut acceptor, user_channel_id, io_channel_id); + assert_eq!(result.alternate_shell, "curtain"); + assert_eq!(result.work_dir, "C:\\work"); +} + /// The client's GCC blocks with `desktop` and `device` as the `desktopScaleFactor` and `deviceScaleFactor` of its /// Client Core Data, and the optional fields that precede them filled in so that they are encoded. fn client_blocks_with_scale_factors(desktop: Option, device: Option) -> ironrdp_pdu::gcc::ClientGccBlocks { @@ -2026,6 +2057,75 @@ async fn credssp_failed_logon_is_reported_as_logon_failure( assert_eq!(ts_request.error_code, Some(NStatusCode::LOGON_FAILURE)); } +/// With several accepted credentials, CredSSP checks the password against the entry with the +/// client's user name, and only that one. +#[rstest::rstest] +#[case::first_user("alice", "secret", true)] +#[case::second_user("bob", "hunter2", true)] +#[case::other_users_password("bob", "secret", false)] +#[case::unknown_user("carol", "secret", false)] +#[tokio::test] +async fn credssp_accepts_any_of_several_credentials( + #[case] username: &str, + #[case] password: &str, + #[case] accepted: bool, + #[values(SecurityProtocol::HYBRID, SecurityProtocol::HYBRID_EX)] protocol: SecurityProtocol, +) { + let mut acceptor = negotiated_acceptor_expecting(protocol, None); + acceptor.set_credential_list(vec![ + Credentials { + username: "alice".to_owned(), + password: "secret".to_owned(), + domain: None, + }, + Credentials { + username: "bob".to_owned(), + password: "hunter2".to_owned(), + domain: None, + }, + ]); + let public_key = vec![0x42; 32]; + let (mut client, request) = CredsspSequence::init( + ironrdp_connector::Credentials::UsernamePassword { + username: username.to_owned(), + password: password.to_owned(), + }, + None, + protocol, + "server.example.com".to_owned().into(), + public_key.clone(), + None, + ) + .unwrap(); + let first_message = client_credssp_message(&mut client, request); + let mut framed = Framed::::new(HandshakeStream { + incoming: [first_message].into(), + credssp_client: Some(client), + ..HandshakeStream::default() + }); + let mut handler = SetupHandler::default(); + let result = accept_credssp_with( + &mut framed, + &mut acceptor, + &mut NoNetwork, + "client.example.com".to_owned().into(), + public_key, + None, + &mut handler, + ) + .await; + + assert_eq!(result.is_ok(), accepted, "{result:?}"); + // With HYBRID_EX the delegated credentials reach the handler during CredSSP. + if protocol == SecurityProtocol::HYBRID_EX && accepted { + assert_eq!(handler.received.len(), 1); + assert_eq!(handler.received[0].credentials.username, username); + assert_eq!(handler.received[0].credentials.password, password); + } else if !accepted { + assert!(handler.received.is_empty()); + } +} + /// The server Demand Active carries `RDP` as its source descriptor, as the FreeRDP /// server does; some clients (Jump Desktop) refuse an empty one. #[test]