feat(acceptor): add CredSSP support

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
This commit is contained in:
Marc-André Lureau 2024-10-23 22:35:48 +04:00 committed by Benoît Cortier
parent ac24e15a3d
commit 4c4d93bc6f
3 changed files with 390 additions and 25 deletions

View file

@ -27,7 +27,7 @@ const IO_CHANNEL_ID: u16 = 1003;
const USER_CHANNEL_ID: u16 = 1002;
pub struct Acceptor {
state: AcceptorState,
pub(crate) state: AcceptorState,
security: nego::SecurityProtocol,
io_channel_id: u16,
user_channel_id: u16,
@ -35,7 +35,7 @@ pub struct Acceptor {
server_capabilities: Vec<CapabilitySet>,
static_channels: StaticChannelSet,
saved_for_reactivation: AcceptorState,
creds: Option<Credentials>,
pub(crate) creds: Option<Credentials>,
}
#[derive(Debug)]
@ -128,6 +128,23 @@ impl Acceptor {
}
}
pub fn mark_security_upgrade_as_done(&mut self) {
assert!(self.reached_security_upgrade().is_some());
self.step(&[], &mut WriteBuf::new()).expect("transition to next state");
debug_assert!(self.reached_security_upgrade().is_none());
}
pub fn should_perform_credssp(&self) -> bool {
matches!(self.state, AcceptorState::Credssp { .. })
}
pub fn mark_credssp_as_done(&mut self) {
assert!(self.should_perform_credssp());
let res = self.step(&[], &mut WriteBuf::new()).expect("transition to next state");
debug_assert!(!self.should_perform_credssp());
assert_eq!(res, Written::Nothing);
}
pub fn get_result(&mut self) -> Option<AcceptorResult> {
match mem::take(&mut self.state) {
AcceptorState::Accepted {
@ -160,25 +177,35 @@ pub enum AcceptorState {
},
SecurityUpgrade {
requested_protocol: nego::SecurityProtocol,
protocol: nego::SecurityProtocol,
},
Credssp {
requested_protocol: nego::SecurityProtocol,
protocol: nego::SecurityProtocol,
},
BasicSettingsWaitInitial {
requested_protocol: nego::SecurityProtocol,
protocol: nego::SecurityProtocol,
},
BasicSettingsSendResponse {
requested_protocol: nego::SecurityProtocol,
protocol: nego::SecurityProtocol,
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
channels: Vec<(u16, Option<gcc::ChannelDef>)>,
},
ChannelConnection {
protocol: nego::SecurityProtocol,
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
channels: Vec<(u16, gcc::ChannelDef)>,
connection: ChannelConnectionSequence,
},
RdpSecurityCommencement {
protocol: nego::SecurityProtocol,
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
channels: Vec<(u16, gcc::ChannelDef)>,
},
SecureSettingsExchange {
protocol: nego::SecurityProtocol,
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
channels: Vec<(u16, gcc::ChannelDef)>,
},
@ -215,6 +242,7 @@ impl State for AcceptorState {
Self::InitiationWaitRequest => "InitiationWaitRequest",
Self::InitiationSendConfirm { .. } => "InitiationSendConfirm",
Self::SecurityUpgrade { .. } => "SecurityUpgrade",
Self::Credssp { .. } => "Credssp",
Self::BasicSettingsWaitInitial { .. } => "BasicSettingsWaitInitial",
Self::BasicSettingsSendResponse { .. } => "BasicSettingsSendResponse",
Self::ChannelConnection { .. } => "ChannelConnection",
@ -245,6 +273,7 @@ impl Sequence for Acceptor {
AcceptorState::InitiationWaitRequest => Some(&pdu::X224_HINT),
AcceptorState::InitiationSendConfirm { .. } => None,
AcceptorState::SecurityUpgrade { .. } => None,
AcceptorState::Credssp { .. } => None,
AcceptorState::BasicSettingsWaitInitial { .. } => Some(&pdu::X224_HINT),
AcceptorState::BasicSettingsSendResponse { .. } => None,
AcceptorState::ChannelConnection { connection, .. } => connection.next_pdu_hint(),
@ -281,9 +310,10 @@ impl Sequence for Acceptor {
}
AcceptorState::InitiationSendConfirm { requested_protocol } => {
let protocol = requested_protocol & self.security;
let connection_confirm = nego::ConnectionConfirm::Response {
flags: nego::ResponseFlags::empty(),
protocol: self.security,
protocol,
};
debug!(message = ?connection_confirm, "Send");
@ -293,16 +323,48 @@ impl Sequence for Acceptor {
(
Written::from_size(written)?,
AcceptorState::SecurityUpgrade { requested_protocol },
AcceptorState::SecurityUpgrade {
requested_protocol,
protocol,
},
)
}
AcceptorState::SecurityUpgrade { requested_protocol } => (
AcceptorState::SecurityUpgrade {
requested_protocol,
protocol,
} => {
debug!(?requested_protocol);
let next_state =
if protocol.intersects(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX) {
AcceptorState::Credssp {
requested_protocol,
protocol,
}
} else {
AcceptorState::BasicSettingsWaitInitial {
requested_protocol,
protocol,
}
};
(Written::Nothing, next_state)
}
AcceptorState::Credssp {
requested_protocol,
protocol,
} => (
Written::Nothing,
AcceptorState::BasicSettingsWaitInitial { requested_protocol },
AcceptorState::BasicSettingsWaitInitial {
requested_protocol,
protocol,
},
),
AcceptorState::BasicSettingsWaitInitial { requested_protocol } => {
AcceptorState::BasicSettingsWaitInitial {
requested_protocol,
protocol,
} => {
let x224_payload = decode::<X224<pdu::x224::X224Data<'_>>>(input)
.map_err(ConnectorError::decode)
.map(|p| p.0)?;
@ -354,6 +416,7 @@ impl Sequence for Acceptor {
Written::Nothing,
AcceptorState::BasicSettingsSendResponse {
requested_protocol,
protocol,
early_capability,
channels,
},
@ -362,6 +425,7 @@ impl Sequence for Acceptor {
AcceptorState::BasicSettingsSendResponse {
requested_protocol,
protocol,
early_capability,
channels,
} => {
@ -394,6 +458,7 @@ impl Sequence for Acceptor {
(
Written::from_size(written)?,
AcceptorState::ChannelConnection {
protocol,
early_capability,
channels,
connection: if skip_channel_join {
@ -406,6 +471,7 @@ impl Sequence for Acceptor {
}
AcceptorState::ChannelConnection {
protocol,
early_capability,
channels,
mut connection,
@ -413,11 +479,13 @@ impl Sequence for Acceptor {
let written = connection.step(input, output)?;
let state = if connection.is_done() {
AcceptorState::RdpSecurityCommencement {
protocol,
early_capability,
channels,
}
} else {
AcceptorState::ChannelConnection {
protocol,
early_capability,
channels,
connection,
@ -428,18 +496,21 @@ impl Sequence for Acceptor {
}
AcceptorState::RdpSecurityCommencement {
protocol,
early_capability,
channels,
..
} => (
Written::Nothing,
AcceptorState::SecureSettingsExchange {
protocol,
early_capability,
channels,
},
),
AcceptorState::SecureSettingsExchange {
protocol,
early_capability,
channels,
} => {
@ -450,28 +521,29 @@ impl Sequence for Acceptor {
debug!(message = ?client_info, "Received");
let creds = client_info.client_info.credentials;
if !protocol.intersects(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX) {
let creds = client_info.client_info.credentials;
if self.creds.as_ref().map_or(true, |srv_creds| srv_creds != &creds) {
// how authorization should be denied with standard RDP security?
let info = ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
ProtocolIndependentCode::ServerDeniedConnection,
));
if self.creds.as_ref().map_or(true, |srv_creds| srv_creds != &creds) {
// how authorization should be denied with standard RDP security?
let info = ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
ProtocolIndependentCode::ServerDeniedConnection,
));
debug!(message = ?info, "Send");
debug!(message = ?info, "Send");
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &info, output)?;
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &info, output)?;
return Err(ConnectorError::general("invalid credentials"));
} else {
(
Written::Nothing,
AcceptorState::LicensingExchange {
early_capability,
channels,
},
)
return Err(ConnectorError::general("invalid credentials"));
}
}
(
Written::Nothing,
AcceptorState::LicensingExchange {
early_capability,
channels,
},
)
}
AcceptorState::LicensingExchange {

View file

@ -0,0 +1,160 @@
use ironrdp_connector::{
credssp::KerberosConfig,
custom_err, general_err,
sspi::{
self,
credssp::{ClientMode, CredSspServer, CredentialsProxy, ServerError, ServerState, TsRequest},
negotiate::ProtocolConfig,
AuthIdentity, Username,
},
ConnectorError, ConnectorErrorKind, ConnectorResult, ServerName, Written,
};
use ironrdp_core::{other_err, WriteBuf};
use ironrdp_pdu::PduHint;
#[derive(Debug)]
pub(crate) enum CredsspState {
Ongoing,
Finished,
ServerError(sspi::Error),
}
#[derive(Clone, Copy, Debug)]
struct CredsspTsRequestHint;
const CREDSSP_TS_REQUEST_HINT: CredsspTsRequestHint = CredsspTsRequestHint;
impl PduHint for CredsspTsRequestHint {
fn find_size(&self, bytes: &[u8]) -> ironrdp_core::DecodeResult<Option<(bool, usize)>> {
match TsRequest::read_length(bytes) {
Ok(length) => Ok(Some((true, length))),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(other_err!("CredsspTsRequestHint", source: e)),
}
}
}
#[derive(Debug)]
pub(crate) struct CredsspSequence<'a> {
server: CredSspServer<CredentialsProxyImpl<'a>>,
state: CredsspState,
// selected_protocol: nego::SecurityProtocol,
}
#[derive(Debug)]
struct CredentialsProxyImpl<'a> {
credentials: &'a AuthIdentity,
}
impl<'a> CredentialsProxyImpl<'a> {
fn new(credentials: &'a AuthIdentity) -> Self {
Self { credentials }
}
}
impl CredentialsProxy for CredentialsProxyImpl<'_> {
type AuthenticationData = AuthIdentity;
fn auth_data_by_user(&mut self, username: &Username) -> std::io::Result<Self::AuthenticationData> {
if username.account_name() != self.credentials.username.account_name() {
return Err(std::io::Error::other("invalid username"));
}
let mut data = self.credentials.clone();
// keep the original user/domain
data.username = username.clone();
Ok(data)
}
}
impl<'a> CredsspSequence<'a> {
pub(crate) fn next_pdu_hint(&self) -> ConnectorResult<Option<&dyn PduHint>> {
match &self.state {
CredsspState::Ongoing => Ok(Some(&CREDSSP_TS_REQUEST_HINT)),
CredsspState::Finished => Ok(None),
CredsspState::ServerError(err) => Err(custom_err!("Credssp server error", err.clone())),
}
}
pub(crate) fn init(
creds: &'a AuthIdentity,
client_computer_name: ServerName,
public_key: Vec<u8>,
kerberos_config: Option<KerberosConfig>,
) -> ConnectorResult<Self> {
let client_computer_name = client_computer_name.into_inner();
let credentials = CredentialsProxyImpl::new(creds);
let credssp_config: Box<dyn ProtocolConfig>;
if let Some(ref krb_config) = kerberos_config {
credssp_config = Box::new(Into::<sspi::KerberosConfig>::into(krb_config.clone()));
} else {
credssp_config = Box::<sspi::ntlm::NtlmConfig>::default();
}
debug!(?credssp_config);
let server = CredSspServer::new(
public_key,
credentials,
ClientMode::Negotiate(sspi::NegotiateConfig {
protocol_config: credssp_config,
package_list: None,
client_computer_name,
}),
)
.map_err(|e| ConnectorError::new("CredSSP", ConnectorErrorKind::Credssp(e)))?;
let sequence = Self {
server,
state: CredsspState::Ongoing,
};
Ok(sequence)
}
/// Returns Some(ts_request) when a TS request is received from client,
pub(crate) fn decode_client_message(&mut self, input: &[u8]) -> ConnectorResult<Option<TsRequest>> {
match self.state {
CredsspState::Ongoing => {
let message = TsRequest::from_buffer(input).map_err(|e| custom_err!("TsRequest", e))?;
debug!(?message, "Received");
Ok(Some(message))
}
_ => Err(general_err!(
"attempted to feed client request to CredSSP sequence in an unexpected state"
)),
}
}
pub(crate) fn process_ts_request(&mut self, request: TsRequest) -> Result<ServerState, Box<ServerError>> {
Ok(self.server.process(request)?)
}
pub(crate) fn handle_process_result(
&mut self,
result: Result<ServerState, Box<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),
Err(err) => (Some(err.ts_request), CredsspState::ServerError(err.error)),
};
self.state = next_state;
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);
ts_request
.encode_ts_request(unfilled_buffer)
.map_err(|e| custom_err!("TsRequest", e))?;
output.advance(length);
Ok(Written::from_size(length)?)
} else {
Ok(Written::Nothing)
}
}
}

View file

@ -3,15 +3,20 @@ extern crate tracing;
use ironrdp_async::bytes::Bytes;
use ironrdp_async::{single_sequence_step, Framed, FramedRead, FramedWrite, StreamWrapper};
use ironrdp_connector::ConnectorResult;
use ironrdp_connector::credssp::KerberosConfig;
use ironrdp_connector::sspi::credssp::EarlyUserAuthResult;
use ironrdp_connector::sspi::{AuthIdentity, Username};
use ironrdp_connector::{custom_err, ConnectorResult, ServerName};
use ironrdp_core::WriteBuf;
mod channel_connection;
mod connection;
mod credssp;
mod finalization;
mod util;
pub use ironrdp_connector::DesktopSize;
use ironrdp_pdu::nego;
pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
pub use self::connection::{Acceptor, AcceptorResult, AcceptorState};
@ -46,6 +51,33 @@ where
}
}
pub async fn accept_credssp<S>(
framed: &mut Framed<S>,
acceptor: &mut Acceptor,
client_computer_name: ServerName,
public_key: Vec<u8>,
kerberos_config: Option<KerberosConfig>,
) -> ConnectorResult<()>
where
S: FramedRead + FramedWrite,
{
let mut buf = WriteBuf::new();
if acceptor.should_perform_credssp() {
perform_credssp_step(
framed,
acceptor,
&mut buf,
client_computer_name,
public_key,
kerberos_config,
)
.await
} else {
Ok(())
}
}
pub async fn accept_finalize<S>(
mut framed: Framed<S>,
acceptor: &mut Acceptor,
@ -63,3 +95,104 @@ where
single_sequence_step(&mut framed, acceptor, &mut buf, unmatched.as_deref_mut()).await?;
}
}
#[instrument(level = "trace", skip_all, ret)]
async fn perform_credssp_step<S>(
framed: &mut Framed<S>,
acceptor: &mut Acceptor,
buf: &mut WriteBuf,
client_computer_name: ServerName,
public_key: Vec<u8>,
kerberos_config: Option<KerberosConfig>,
) -> ConnectorResult<()>
where
S: FramedRead + FramedWrite,
{
assert!(acceptor.should_perform_credssp());
let AcceptorState::Credssp { protocol, .. } = acceptor.state else {
unreachable!()
};
async fn credssp_loop<S>(
framed: &mut Framed<S>,
acceptor: &mut Acceptor,
buf: &mut WriteBuf,
client_computer_name: ServerName,
public_key: Vec<u8>,
kerberos_config: Option<KerberosConfig>,
) -> ConnectorResult<()>
where
S: FramedRead + FramedWrite,
{
let creds = acceptor.creds.as_ref().unwrap();
let username = Username::new(&creds.username, None).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)?;
loop {
let Some(next_pdu_hint) = sequence.next_pdu_hint()? else {
break;
};
debug!(
acceptor.state = ?acceptor.state,
hint = ?next_pdu_hint,
"Wait for PDU"
);
let pdu = framed
.read_by_hint(next_pdu_hint, None)
.await
.map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?;
trace!(length = pdu.len(), "PDU received");
let Some(ts_request) = sequence.decode_client_message(&pdu)? else {
break;
};
let result = sequence.process_ts_request(ts_request);
buf.clear();
let written = sequence.handle_process_result(result, buf)?;
if let Some(response_len) = written.size() {
let response = &buf[..response_len];
trace!(response_len, "Send response");
framed
.write_all(response)
.await
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
}
}
Ok(())
}
let result = match credssp_loop(framed, acceptor, buf, client_computer_name, public_key, kerberos_config).await {
Ok(_) => EarlyUserAuthResult::Success,
Err(err) => {
warn!("credssp: {err}");
EarlyUserAuthResult::AccessDenied
}
};
if protocol.intersects(nego::SecurityProtocol::HYBRID_EX) {
trace!(?result, "HYBRID_EX");
buf.clear();
result
.to_buffer(&mut *buf)
.map_err(|e| ironrdp_connector::custom_err!("to_buffer", e))?;
let response = &buf[..result.buffer_len()];
framed
.write_all(response)
.await
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
}
acceptor.mark_credssp_as_done();
Ok(())
}