clients::rdp connects over any caller-dialled TCP stream (direct, proxy or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM), optionally falling back to TLS-only logon for servers without NLA such as xrdp (credentials are passed so they log straight in). The server certificate is pinned through a caller check on its SHA256 fingerprint before any credentials are sent. The desktop decodes into the VNC client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse operations), Display Control resizes and the deactivation-reactivation sequence run on the session thread. IronRDP's releases need three vendored patches (vendor/README.md): picky and sspi with their RustCrypto release-candidate pins relaxed so they share russh's stable versions (their own suites, 402 tests, pass against them), and ironrdp-session with two unreleased upstream bitmap fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops render sheared. Verified live against xrdp (scottyhardy/docker-remote-desktop): logon, full desktop rendering, keyboard and mouse input.
211 lines
6.7 KiB
Rust
211 lines
6.7 KiB
Rust
use std::io;
|
|
use std::sync::LazyLock;
|
|
|
|
use sspi::{
|
|
AcquireCredentialsHandleResult, AuthIdentity, BufferType, ClientRequestFlags, ContextNames, CredentialUse,
|
|
DataRepresentation, EncryptionFlags, SecurityBuffer, SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi,
|
|
SspiEx, Username, credssp,
|
|
};
|
|
use time::OffsetDateTime;
|
|
|
|
pub(crate) static CREDENTIALS: LazyLock<AuthIdentity> = LazyLock::new(|| AuthIdentity {
|
|
username: Username::new("Username", Some("Domain")).unwrap(),
|
|
password: String::from("Password").into(),
|
|
});
|
|
|
|
const MESSAGE_TO_CLIENT: &[u8] = b"Hello, client!";
|
|
|
|
pub(crate) struct CredentialsProxyImpl<'a> {
|
|
credentials: &'a AuthIdentity,
|
|
}
|
|
|
|
impl<'a> CredentialsProxyImpl<'a> {
|
|
pub(crate) fn new(credentials: &'a AuthIdentity) -> Self {
|
|
Self { credentials }
|
|
}
|
|
}
|
|
|
|
impl credssp::CredentialsProxy for CredentialsProxyImpl<'_> {
|
|
type AuthenticationData = AuthIdentity;
|
|
|
|
fn auth_data_by_user(&mut self, username: &Username) -> io::Result<Self::AuthenticationData> {
|
|
assert_eq!(username.account_name(), self.credentials.username.account_name());
|
|
|
|
Ok(self.credentials.clone())
|
|
}
|
|
|
|
fn auth_data(&mut self) -> io::Result<Vec<Self::AuthenticationData>> {
|
|
Ok(vec![self.credentials.clone()])
|
|
}
|
|
}
|
|
|
|
pub(crate) fn create_client_credentials_handle<T>(
|
|
client: &mut T,
|
|
auth_data: Option<&T::AuthenticationData>,
|
|
) -> sspi::Result<T::CredentialsHandle>
|
|
where
|
|
T: Sspi,
|
|
{
|
|
let AcquireCredentialsHandleResult {
|
|
credentials_handle,
|
|
expiry,
|
|
} = if let Some(auth_data) = auth_data {
|
|
client
|
|
.acquire_credentials_handle()
|
|
.with_credential_use(CredentialUse::Outbound)
|
|
.with_auth_data(auth_data)
|
|
.execute(client)?
|
|
} else {
|
|
client
|
|
.acquire_credentials_handle()
|
|
.with_credential_use(CredentialUse::Outbound)
|
|
.execute(client)?
|
|
};
|
|
|
|
if let Some(expiry) = expiry {
|
|
let now = OffsetDateTime::now_utc();
|
|
assert!(now < expiry);
|
|
}
|
|
|
|
Ok(credentials_handle)
|
|
}
|
|
|
|
pub(crate) fn create_server_credentials_handle<T>(server: &mut T) -> sspi::Result<T::CredentialsHandle>
|
|
where
|
|
T: Sspi,
|
|
{
|
|
let AcquireCredentialsHandleResult {
|
|
credentials_handle,
|
|
expiry,
|
|
} = server
|
|
.acquire_credentials_handle()
|
|
.with_credential_use(CredentialUse::Inbound)
|
|
.execute(server)?;
|
|
if let Some(expiry) = expiry {
|
|
let now = OffsetDateTime::now_utc();
|
|
assert!(now < expiry);
|
|
}
|
|
|
|
Ok(credentials_handle)
|
|
}
|
|
|
|
pub(crate) fn process_authentication_without_complete<ClientSspi, ServerSspi>(
|
|
client: &mut ClientSspi,
|
|
mut client_creds_handle: ClientSspi::CredentialsHandle,
|
|
server: &mut ServerSspi,
|
|
mut server_creds_handle: ServerSspi::CredentialsHandle,
|
|
) -> sspi::Result<(SecurityStatus, SecurityStatus)>
|
|
where
|
|
ClientSspi: Sspi,
|
|
ServerSspi: Sspi,
|
|
{
|
|
let mut server_output = Vec::new();
|
|
let mut client_status;
|
|
let mut server_status = SecurityStatus::ContinueNeeded;
|
|
|
|
loop {
|
|
let mut client_output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
|
|
|
let mut builder = client
|
|
.initialize_security_context()
|
|
.with_credentials_handle(&mut client_creds_handle)
|
|
.with_context_requirements(ClientRequestFlags::ALLOCATE_MEMORY | ClientRequestFlags::CONFIDENTIALITY)
|
|
.with_target_data_representation(DataRepresentation::Native)
|
|
.with_input(&mut server_output)
|
|
.with_output(&mut client_output);
|
|
|
|
let client_result = client
|
|
.initialize_security_context_impl(&mut builder)?
|
|
.resolve_to_result()?;
|
|
client_status = client_result.status;
|
|
|
|
if client_status != SecurityStatus::ContinueNeeded && server_status != SecurityStatus::ContinueNeeded {
|
|
return Ok((client_status, server_status));
|
|
}
|
|
|
|
server_output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
|
|
|
let builder = server
|
|
.accept_security_context()
|
|
.with_credentials_handle(&mut server_creds_handle)
|
|
.with_context_requirements(ServerRequestFlags::ALLOCATE_MEMORY)
|
|
.with_target_data_representation(DataRepresentation::Native)
|
|
.with_input(&mut client_output)
|
|
.with_output(&mut server_output);
|
|
let server_result = server.accept_security_context_impl(builder)?.resolve_to_result()?;
|
|
server_status = server_result.status;
|
|
|
|
if client_status != SecurityStatus::ContinueNeeded && server_status != SecurityStatus::ContinueNeeded {
|
|
return Ok((client_status, server_status));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn try_complete_authentication<T>(server: &mut T, auth_server_status: SecurityStatus) -> sspi::Result<()>
|
|
where
|
|
T: Sspi,
|
|
{
|
|
if auth_server_status == SecurityStatus::CompleteNeeded || auth_server_status == SecurityStatus::CompleteAndContinue
|
|
{
|
|
let mut token = Vec::new();
|
|
server.complete_auth_token(&mut token)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn set_identity_and_try_complete_authentication<T, C>(
|
|
server: &mut T,
|
|
auth_server_status: SecurityStatus,
|
|
credentials_proxy: &mut C,
|
|
) -> sspi::Result<()>
|
|
where
|
|
T: Sspi + SspiEx,
|
|
C: credssp::CredentialsProxy<AuthenticationData = T::AuthenticationData>,
|
|
{
|
|
if auth_server_status == SecurityStatus::CompleteNeeded || auth_server_status == SecurityStatus::CompleteAndContinue
|
|
{
|
|
let ContextNames { username } = server.query_context_names()?;
|
|
let auth_data = credentials_proxy.auth_data_by_user(&username)?;
|
|
server.custom_set_auth_identity(auth_data).unwrap();
|
|
|
|
let mut token = Vec::new();
|
|
server.complete_auth_token(&mut token)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn check_messages_encryption(client: &mut impl Sspi, server: &mut impl Sspi) -> sspi::Result<()> {
|
|
let server_sizes = server.query_context_sizes()?;
|
|
|
|
let mut token = vec![0; server_sizes.security_trailer as usize];
|
|
let mut data = MESSAGE_TO_CLIENT.to_vec();
|
|
let mut messages = [
|
|
SecurityBufferRef::token_buf(token.as_mut_slice()),
|
|
SecurityBufferRef::data_buf(data.as_mut_slice()),
|
|
];
|
|
server.encrypt_message(EncryptionFlags::empty(), &mut messages)?;
|
|
assert_ne!(MESSAGE_TO_CLIENT, messages[1].data());
|
|
|
|
println!(
|
|
"Message to client: {:x?}, encrypted message: {:x?}, token: {:x?}",
|
|
MESSAGE_TO_CLIENT,
|
|
messages[0].data(),
|
|
messages[1].data()
|
|
);
|
|
|
|
let [mut token, mut data] = messages;
|
|
|
|
let mut messages = vec![
|
|
SecurityBufferRef::data_buf(data.take_data()),
|
|
SecurityBufferRef::token_buf(token.take_data()),
|
|
];
|
|
|
|
client.decrypt_message(&mut messages)?;
|
|
|
|
assert_eq!(MESSAGE_TO_CLIENT, messages[0].data());
|
|
|
|
Ok(())
|
|
}
|