Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 59 additions & 10 deletions crates/ironrdp-acceptor/src/connection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use core::mem;

use ironrdp_connector::sspi::AuthIdentity;
use ironrdp_connector::{
ConnectorError, ConnectorErrorExt as _, ConnectorResult, DesktopSize, Sequence, State, Written, encode_x224_packet,
general_err, reason_err,
Expand Down Expand Up @@ -36,7 +37,7 @@ pub struct Acceptor {
static_channels: StaticChannelSet,
saved_for_reactivation: AcceptorState,
pub(crate) creds: Option<Credentials>,
received_credentials: Option<Credentials>,
received_credentials: Option<ReceivedCredentials>,
reactivation: bool,
honor_client_desktop_size: bool,
}
Expand Down Expand Up @@ -73,6 +74,35 @@ fn set_bitmap_desktop_size(capabilities: &mut [CapabilitySet], size: DesktopSize
}
}

/// Protocol source and handshake-authentication status of received credentials.
///
/// Servers must not infer this from their configured security mode: the origin
/// records what the acceptor actually received during negotiation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CredentialOrigin {
/// Received in the ClientInfoPdu (MS-RDPBCGR 2.2.1.11).
///
/// These credentials are client-supplied and have not been authenticated by
/// the protocol handshake. A server should validate them before using them
/// to select identity-bound resources.
ClientInfo,
/// Delegated TSPasswordCreds decrypted by CredSSP (MS-CSSP).
///
/// CredSSP authenticated the principal during the exchange. A server may
/// still run authorization policy before starting or selecting a session.
CredSspDelegated,
}

/// Credentials received by the acceptor together with their protocol origin.
///
/// Keeping both values in one type makes it impossible to expose credentials
/// without the provenance required to interpret their authentication status.
#[derive(Debug)]
pub struct ReceivedCredentials {
pub credentials: Credentials,
pub origin: CredentialOrigin,
}

#[derive(Debug)]
pub struct AcceptorResult {
pub static_channels: StaticChannelSet,
Expand All @@ -96,15 +126,17 @@ pub struct AcceptorResult {
/// announce one. Servers can use it to pick a server-side keyboard layout
/// matching the client without changing any local input state.
pub keyboard_layout: u32,
/// Credentials received from the client during SecureSettingsExchange.
/// Credentials received from the client together with their origin.
///
/// Present for TLS-mode connections where the client sends credentials
/// in the ClientInfoPdu. `None` for CredSSP/Hybrid connections (where
/// authentication happens during the CredSSP exchange instead).
/// For TLS/Standard connections, this contains credentials sent later in
/// the ClientInfoPdu and marks them as unauthenticated by the handshake.
/// For CredSSP/Hybrid connections, it contains the delegated TSPasswordCreds
/// decrypted by the CredSSP state machine and marks them as authenticated by
/// that exchange.
///
/// Servers that need to validate credentials (e.g., via PAM or LDAP)
/// can use this field for post-handshake validation.
pub credentials: Option<Credentials>,
/// Embedding servers can use the value for post-handshake validation or
/// authorization and for selecting per-user session resources.
pub received_credentials: Option<ReceivedCredentials>,
}

impl Acceptor {
Expand Down Expand Up @@ -231,6 +263,20 @@ impl Acceptor {
matches!(self.state, AcceptorState::Credssp { .. })
}

/// Store credentials delegated by CredSSP/NLA so server code can use the
/// same post-handshake validation and binding path as TLS ClientInfo
/// credentials.
pub(crate) fn set_received_credssp_credentials(&mut self, identity: AuthIdentity) {
self.received_credentials = Some(ReceivedCredentials {
credentials: Credentials {
username: identity.username.account_name().to_owned(),
password: identity.password.as_ref().clone(),
domain: identity.username.domain_name().map(str::to_owned),
},
origin: CredentialOrigin::CredSspDelegated,
});
}

/// # Panics
///
/// Panics if state is not [AcceptorState::Credssp].
Expand All @@ -256,7 +302,7 @@ impl Acceptor {
message_channel_id: self.message_channel_id,
keyboard_layout: self.keyboard_layout,
reactivation: self.reactivation,
credentials: self.received_credentials.take(),
received_credentials: self.received_credentials.take(),
}),
previous_state => {
self.state = previous_state;
Expand Down Expand Up @@ -710,7 +756,10 @@ impl Sequence for Acceptor {
}

// Store credentials for later retrieval via AcceptorResult.
self.received_credentials = Some(creds);
self.received_credentials = Some(ReceivedCredentials {
credentials: creds,
origin: CredentialOrigin::ClientInfo,
});
}

(
Expand Down
19 changes: 11 additions & 8 deletions crates/ironrdp-acceptor/src/credssp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,19 @@ impl<'a> CredsspSequence<'a> {
&mut self,
result: Result<ServerState, ServerError>,
output: &mut WriteBuf,
) -> ConnectorResult<Written> {
let (ts_request, next_state) = match result {
Ok(ServerState::ReplyNeeded(ts_request)) => (Some(ts_request), CredsspState::Ongoing),
Ok(ServerState::Finished(_id)) => (None, CredsspState::Finished),
) -> ConnectorResult<(Written, Option<AuthIdentity>)> {
let (ts_request, next_state, credentials) = match result {
Ok(ServerState::ReplyNeeded(ts_request)) => (Some(ts_request), CredsspState::Ongoing, None),
Ok(ServerState::Finished(id)) => (None, CredsspState::Finished, Some(id)),
Err(err) => (
err.ts_request.map(|ts_request| *ts_request),
CredsspState::ServerError(err.error),
None,
),
};

self.state = next_state;
if let Some(ts_request) = ts_request {
let written = if let Some(ts_request) = ts_request {
debug!(?ts_request, "Send");
let length = usize::from(ts_request.buffer_len());
let unfilled_buffer = output.unfilled_to(length);
Expand All @@ -175,9 +176,11 @@ impl<'a> CredsspSequence<'a> {

output.advance(length);

Ok(Written::from_size(length)?)
Written::from_size(length)?
} else {
Ok(Written::Nothing)
}
Written::Nothing
};

Ok((written, credentials))
}
}
7 changes: 5 additions & 2 deletions crates/ironrdp-acceptor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub use ironrdp_connector::DesktopSize;
use ironrdp_pdu::nego;

pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
pub use self::connection::{Acceptor, AcceptorResult, AcceptorState};
pub use self::connection::{Acceptor, AcceptorResult, AcceptorState, CredentialOrigin, ReceivedCredentials};
pub use self::finalization::{FinalizationSequence, FinalizationState};
use crate::credssp::resolve_generator;

Expand Down Expand Up @@ -208,7 +208,10 @@ where
}; // drop generator

buf.clear();
let written = sequence.handle_process_result(result, buf)?;
let (written, delegated_credentials) = sequence.handle_process_result(result, buf)?;
if let Some(credentials) = delegated_credentials {
acceptor.set_received_credssp_credentials(credentials);
}

if let Some(response_len) = written.size() {
let response = &buf[..response_len];
Expand Down
30 changes: 20 additions & 10 deletions crates/ironrdp-server/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use super::display::{DesktopSize, RdpServerDisplay};
#[cfg(feature = "egfx")]
use super::gfx::GfxServerFactory;
use super::handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler};
use super::server::{ConnectionHandler, CredentialValidator, RdpServer, RdpServerOptions, RdpServerSecurity};
use super::server::{
ConnectionBinder, ConnectionHandler, CredentialValidator, RdpServer, RdpServerOptions, RdpServerSecurity,
};
use crate::{DisplayUpdate, RdpServerDisplayUpdates, SoundServerFactory};

pub struct WantsAddr {}
Expand All @@ -38,6 +40,7 @@ pub struct BuilderDone {
sound_factory: Option<Box<dyn SoundServerFactory>>,
connection_handler: Option<Box<dyn ConnectionHandler>>,
credential_validator: Option<Arc<dyn CredentialValidator>>,
connection_binder: Option<Arc<dyn ConnectionBinder>>,
#[cfg(feature = "egfx")]
gfx_factory: Option<Box<dyn GfxServerFactory>>,
display_suppressed: Option<Arc<AtomicBool>>,
Expand Down Expand Up @@ -137,6 +140,7 @@ impl RdpServerBuilder<WantsDisplay> {
cliprdr_factory: None,
connection_handler: None,
credential_validator: None,
connection_binder: None,
codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"),
max_request_size: RdpServerOptions::DEFAULT_MAX_REQUEST_SIZE,
#[cfg(feature = "egfx")]
Expand All @@ -159,6 +163,7 @@ impl RdpServerBuilder<WantsDisplay> {
cliprdr_factory: None,
connection_handler: None,
credential_validator: None,
connection_binder: None,
codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"),
max_request_size: RdpServerOptions::DEFAULT_MAX_REQUEST_SIZE,
#[cfg(feature = "egfx")]
Expand Down Expand Up @@ -263,21 +268,25 @@ impl RdpServerBuilder<BuilderDone> {
self
}

/// Set a credential validator for TLS-mode connections.
/// Set a credential validator for accepted client credentials.
///
/// When set, credentials received from the client during
/// `SecureSettingsExchange` (`ClientInfoPdu`) are passed to this
/// validator before the session is established. Rejection or a backend
/// error closes the connection. Pass `None` (the default) to skip
/// validation entirely.
///
/// Not used for CredSSP/Hybrid connections (those use pre-loaded
/// credentials for NTLM challenge-response).
/// When set, credentials surfaced by the acceptor are passed to this
/// validator before the session is established, together with their
/// origin. This includes `SecureSettingsExchange` (`ClientInfoPdu`)
/// credentials and, when available, CredSSP/Hybrid delegated credentials.
/// Rejection or a backend error closes the connection. Pass `None` (the
/// default) to skip validation entirely.
pub fn with_credential_validator(mut self, validator: Option<Arc<dyn CredentialValidator>>) -> Self {
self.state.credential_validator = validator;
self
}

/// Set a binder that replaces display/input handlers after credentials are accepted.
pub fn with_connection_binder(mut self, binder: Option<Arc<dyn ConnectionBinder>>) -> Self {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: The Option param reads oddly at the call site (.with_connection_binder(Some(Arc::new(x)))), but it's consistent with with_credential_validator and with_connection_handler. This is consistent, but I’m wondering if we should change that. Out of scope for this PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; I left this unchanged for this PR to stay consistent with the adjacent optional hooks and avoid mixing API ergonomics into the correctness fixes.

self.state.connection_binder = binder;
Comment thread
CBenoit marked this conversation as resolved.
self
}

/// Inject a shared NetworkAutoDetect RTT handle (milliseconds, `u32::MAX`
/// until the first measurement). The server writes the latest measured RTT
/// to the same instance the backend reads. When not called, the server
Expand Down Expand Up @@ -309,6 +318,7 @@ impl RdpServerBuilder<BuilderDone> {
self.state.autodetect_rtt,
);
server.set_credential_validator(self.state.credential_validator);
server.set_connection_binder(self.state.connection_binder);
server
}
}
Expand Down
7 changes: 4 additions & 3 deletions crates/ironrdp-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ pub use gfx::{EgfxServerMessage, GfxDvcBridge, GfxServerFactory, GfxServerHandle
pub use handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler};
#[cfg(feature = "helper")]
pub use helper::TlsIdentityCtx;
pub use ironrdp_acceptor::{CredentialOrigin, ReceivedCredentials};
pub use server::{
ConnectionHandler, CredentialDecision, CredentialValidationError, CredentialValidator, Credentials,
ExactMatchCredentialValidator, PostConnectionAction, RdpServer, RdpServerOptions, RdpServerSecurity, ServerEvent,
ServerEventSender, TransportTls,
BoundConnection, ConnectionBinder, ConnectionHandler, CredentialDecision, CredentialValidationError,
CredentialValidator, Credentials, ExactMatchCredentialValidator, PostConnectionAction, RdpServer, RdpServerOptions,
RdpServerSecurity, ServerEvent, ServerEventSender, TransportTls,
};
pub use sound::{RdpsndServerHandler, RdpsndServerMessage, SoundServerFactory};

Expand Down
Loading