feat(core): RDP client on IronRDP

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.
This commit is contained in:
Andy Dixon
2026-10-12 10:50:24 +01:00
parent 6a4d7381d9
commit 50c0104d57
227 changed files with 66687 additions and 20 deletions
+329
View File
@@ -0,0 +1,329 @@
use std::mem;
use picky_krb::constants::gss_api::{ACCEPT_COMPLETE, ACCEPT_INCOMPLETE};
use picky_krb::gss_api::{NegTokenTarg, NegTokenTarg1};
use crate::generator::YieldPointLocal;
use crate::kerberos::client::principal::get_client_principal_name;
use crate::negotiate::NegotiateState;
use crate::negotiate::generators::{
generate_final_neg_token_targ, generate_mech_type_list, generate_neg_token_init, generate_neg_token_targ_1,
};
use crate::{
AuthIdentity, BufferType, ClientRequestFlags, ClientResponseFlags, CredentialsBuffers, Error, ErrorKind,
InitializeSecurityContextResult, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, SecurityStatus, SspiImpl,
};
/// If the Kerberos context returns an Error, then we check for `error_type` and see if we can fallback to NTLM.
/// We fallback to NTLM in following cases:
/// - [ErrorKind::TimeSkew]: The time skew on KDC and client machines is too big.
/// - [ErrorKind::NoAuthenticatingAuthority]: The Kerberos returns this error type when there is a problem with network client.
/// For example, the network client cannot connect to the KDC, or the KDC proxy returns an error.
/// Also, the `KDC_ERR_WRONG_REALM` Kerberos error code is mapped to this error type, so if the client is misconfigured
/// and tries to get a TGT for a wrong realm, we will fallback to NTLM as well.
/// - [ErrorKind::CertificateUnknown]: The KDC proxy certificate is invalid.
/// - [ErrorKind::UnknownCredentials]: The Kerberos client returns this error kind when KDC replies with `KDC_ERR_S_PRINCIPAL_UNKNOWN` error code.
pub(crate) const NTLM_FALLBACK_ERROR_KINDS: [ErrorKind; 4] = [
ErrorKind::TimeSkew,
ErrorKind::NoAuthenticatingAuthority,
ErrorKind::CertificateUnknown,
ErrorKind::UnknownCredentials,
];
#[cfg(feature = "__test-data")]
pub const FALLBACK_ERROR_KINDS: [ErrorKind; 4] = NTLM_FALLBACK_ERROR_KINDS;
/// Performs one authentication step.
///
/// The user should call this function until it returns `SecurityStatus::Ok`.
#[instrument(ret, fields(protocol = negotiate.protocol_name()), skip_all)]
pub(crate) async fn initialize_security_context<'a>(
negotiate: &'a mut Negotiate,
yield_point: &mut YieldPointLocal,
builder: &'a mut crate::builders::FilledInitializeSecurityContext<
'_,
'_,
<Negotiate as SspiImpl>::CredentialsHandle,
>,
) -> Result<InitializeSecurityContextResult> {
if let Some(target_name) = &builder.target_name {
negotiate.check_target_name_for_ntlm_downgrade(target_name);
}
if let Some(Some(CredentialsBuffers::AuthIdentity(identity))) = builder.credentials_handle {
let auth_identity =
AuthIdentity::try_from(&*identity).map_err(|e| Error::new(ErrorKind::InvalidParameter, e))?;
let account_name = auth_identity.username.account_name();
// `realm_domain` is the per-format "authority" (UPN suffix or NetBIOS domain) used purely
// as a best-effort realm/Azure-AD hint for protocol negotiation, not as an identity.
let domain_name = get_client_principal_name(&auth_identity.username).realm_domain;
negotiate.negotiate_protocol(account_name, domain_name)?;
negotiate.auth_identity = Some(CredentialsBuffers::AuthIdentity(auth_identity.into()));
}
#[cfg(feature = "scard")]
if let Some(Some(CredentialsBuffers::SmartCard(identity))) = builder.credentials_handle {
use crate::NegotiatedProtocol;
if let NegotiatedProtocol::Ntlm(_) = &negotiate.protocol {
// If the user provided smart card credentials, then they definitely want to use Kerberos,
// because NTLM does not support scard logon.
use crate::kerberos::client::principal::get_client_principal_realm;
use crate::{Kerberos, KerberosConfig, detect_kdc_url};
let username = identity.username.to_string();
let host = detect_kdc_url(&get_client_principal_realm(&username, ""))
.ok_or_else(|| Error::new(ErrorKind::NoAuthenticatingAuthority, "can not detect KDC url"))?;
debug!("Negotiate: try Kerberos");
let config = KerberosConfig {
kdc_url: Some(host),
client_computer_name: negotiate.client_computer_name.clone(),
};
negotiate.protocol = NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config(config)?);
}
}
match negotiate.state {
NegotiateState::Initial => {
if builder
.context_requirements
.contains(ClientRequestFlags::USE_SESSION_KEY)
|| builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE)
{
negotiate.mic_needed = true;
negotiate.mic_verified = false;
} else {
negotiate.mic_needed = false;
}
let result = negotiate
.protocol
.initialize_security_context(negotiate.auth_identity.as_ref(), yield_point, builder)
.await;
let first_token = match result {
Ok(result) => {
if result.status != SecurityStatus::ContinueNeeded && result.status != SecurityStatus::Ok {
return Err(Error::new(
ErrorKind::InternalError,
format!("unexpected status: {:?}", result.status),
));
}
let token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
Some(mem::take(&mut token.buffer))
}
Err(err)
if matches!(negotiate.protocol, NegotiatedProtocol::Kerberos(_))
&& NTLM_FALLBACK_ERROR_KINDS.contains(&err.error_type) =>
{
warn!("Kerberos authentication failed with {err} error, attempting NTLM fallback.");
if !negotiate.fallback_to_ntlm() {
warn!("Failed to fallback to NTLM: NTLM is disabled.");
return Err(err);
}
debug!("Fallback to NTLM succeeded");
let result = negotiate
.protocol
.initialize_security_context(negotiate.auth_identity.as_ref(), yield_point, builder)
.await?;
if result.status != SecurityStatus::ContinueNeeded && result.status != SecurityStatus::Ok {
return Err(Error::new(
ErrorKind::InternalError,
format!("unexpected status: {:?}", result.status),
));
}
let token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
Some(mem::take(&mut token.buffer))
}
Err(err) => {
return Err(err);
}
};
let mech_types = generate_mech_type_list(
matches!(&negotiate.protocol, NegotiatedProtocol::Kerberos(_)),
negotiate.package_list.ntlm,
)?;
negotiate.mech_types = picky_asn1_der::to_vec(&mech_types)?;
let encoded_neg_token_init = picky_asn1_der::to_vec(&generate_neg_token_init(mech_types, first_token)?)?;
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
output_token.buffer = encoded_neg_token_init;
negotiate.state = NegotiateState::InProgress;
Ok(InitializeSecurityContextResult {
status: SecurityStatus::ContinueNeeded,
flags: ClientResponseFlags::empty(),
expiry: None,
})
}
NegotiateState::InProgress => {
let input = builder
.input
.as_mut()
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?;
let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?;
let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(input_token.buffer.as_slice())?;
let NegTokenTarg {
neg_result: server_neg_result,
supported_mech,
response_token,
mech_list_mic,
} = neg_token_targ.0;
if let Some(selected_mech) = supported_mech.0 {
let selected_mech = &selected_mech.0;
let mech_type: String = (&selected_mech.0).into();
debug!("The remote server has selected {mech_type} mechanism id.");
negotiate.negotiate_protocol_by_mech_type(
selected_mech,
builder
.credentials_handle
.as_ref()
.and_then(|handle| handle.as_ref().map(|handle| handle.as_auth_identity()))
.and_then(|identity| identity.map(|identity| &identity.user)),
)?;
}
let input_token = SecurityBuffer::find_buffer_mut(input, BufferType::Token)?;
let token = response_token.0.map(|token| token.0.0);
if let Some(token) = token {
input_token.buffer = token;
} else {
input_token.buffer.clear();
}
let mut result = negotiate
.protocol
.initialize_security_context(negotiate.auth_identity.as_ref(), yield_point, builder)
.await?;
if result.status == SecurityStatus::Ok {
if negotiate.mic_needed {
let mech_list_mic = mech_list_mic.0.map(|token| token.0.0);
negotiate.verify_mic_token(mech_list_mic.as_deref())?;
}
let neg_result = if !negotiate.mic_needed || negotiate.mic_verified {
result.status = SecurityStatus::Ok;
negotiate.state = NegotiateState::Ok;
ACCEPT_COMPLETE.to_vec()
} else {
result.status = SecurityStatus::ContinueNeeded;
negotiate.state = NegotiateState::VerifyMic;
ACCEPT_INCOMPLETE.to_vec()
};
let server_neg_result = server_neg_result.0.map(|neg_result| neg_result.0.0);
if server_neg_result.as_deref() == Some(&ACCEPT_COMPLETE) && negotiate.state == NegotiateState::Ok {
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
output_token.buffer.clear();
return Ok(result);
}
prepare_neg_token(neg_result, negotiate, builder)?;
} else {
let token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
let output_token =
picky_asn1_der::to_vec(&generate_neg_token_targ_1(Some(mem::take(&mut token.buffer))))?;
token.buffer = output_token;
}
Ok(result)
}
NegotiateState::VerifyMic => {
let input = builder
.input
.as_mut()
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?;
let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?;
let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(input_token.buffer.as_slice())?;
let NegTokenTarg {
neg_result,
supported_mech: _,
response_token: _,
mech_list_mic,
} = neg_token_targ.0;
let neg_result = neg_result.0.map(|neg_result| neg_result.0.0);
if neg_result.as_deref() != Some(&ACCEPT_COMPLETE) {
return Err(Error::new(
ErrorKind::InvalidToken,
format!("unexpected NegResult: {neg_result:?}"),
));
}
let mech_list_mic = mech_list_mic.0.map(|token| token.0.0);
negotiate.verify_mic_token(mech_list_mic.as_deref())?;
let status = if negotiate.mic_verified {
negotiate.state = NegotiateState::Ok;
SecurityStatus::Ok
} else {
SecurityStatus::ContinueNeeded
};
Ok(InitializeSecurityContextResult {
status,
flags: ClientResponseFlags::empty(),
expiry: None,
})
}
NegotiateState::Ok => Err(Error::new(
ErrorKind::OutOfSequence,
"initialize_security_context called after negotiation completed",
)),
}
}
fn prepare_neg_token(
neg_result: Vec<u8>,
negotiate: &mut Negotiate,
builder: &mut crate::builders::FilledInitializeSecurityContext<'_, '_, <Negotiate as SspiImpl>::CredentialsHandle>,
) -> Result<()> {
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
let response_token = if !output_token.buffer.is_empty() {
Some(mem::take(&mut output_token.buffer))
} else {
None
};
let mic = if negotiate.mic_needed {
Some(
negotiate
.protocol
.generate_mic_token(&negotiate.mech_types, crate::private::Sealed)?,
)
} else {
None
};
let neg_token_targ = generate_final_neg_token_targ(neg_result, response_token, mic);
let encoded_final_neg_token_targ = picky_asn1_der::to_vec(&neg_token_targ)?;
output_token.buffer = encoded_final_neg_token_targ;
Ok(())
}
+62
View File
@@ -0,0 +1,62 @@
use std::fmt::Debug;
use crate::{NegotiatedProtocol, Result};
pub trait ProtocolConfig: Debug + Send + Sync {
fn new_instance(&self) -> Result<NegotiatedProtocol>;
fn box_clone(&self) -> Box<dyn ProtocolConfig>;
}
#[derive(Debug)]
pub struct NegotiateConfig {
pub protocol_config: Box<dyn ProtocolConfig>,
pub package_list: Option<String>,
/// Computer name, or "workstation name", of the client machine performing the authentication attempt
///
/// This is also referred to as the "Source Workstation", i.e.: the name of the computer attempting to logon.
pub client_computer_name: String,
}
impl NegotiateConfig {
/// Creates a new instance of [NegotiateConfig].
///
/// `package_list` specifies allowed security packages for user authorization.
/// Security packages are specified as a comma-separated list of package names in lowercase.
/// If the security package is not allowed, then prepend '!' to its name. Examples:
///
/// - "kerberos,ntlm" - allows both Kerberos and NTLM but not PKU2U.
/// - "kerberos,!ntlm" - allows Kerberos but not NTLM and not PKU2U. Forces the use of Kerberos.
/// - "!kerberos,ntlm" - allows NTLM but not Kerberos and not PKU2U. Forces the use of NTLM.
/// - "!ntlm" - allows Kerberos and PKU2U but not NTLM.
///
/// If `package_list` is None, then all packages are allowed and Kerberos is preferred.
pub fn new(
protocol_config: Box<dyn ProtocolConfig>,
package_list: Option<String>,
client_computer_name: String,
) -> Self {
Self {
protocol_config,
package_list,
client_computer_name,
}
}
pub fn from_protocol_config(protocol_config: Box<dyn ProtocolConfig>, client_computer_name: String) -> Self {
Self {
protocol_config,
package_list: None,
client_computer_name,
}
}
}
impl Clone for NegotiateConfig {
fn clone(&self) -> Self {
Self {
protocol_config: self.protocol_config.box_clone(),
package_list: self.package_list.clone(),
client_computer_name: self.client_computer_name.clone(),
}
}
}
+80
View File
@@ -0,0 +1,80 @@
use oid::ObjectIdentifier;
use picky::oids;
use picky_krb::gss_api::{ApplicationTag0, GssApiNegInit, MechTypeList, NegTokenInit};
use crate::ntlm::NtlmConfig;
use crate::{Error, ErrorKind, Negotiate, NegotiatedProtocol, Ntlm};
/// Extract TGT request and mech types from the first token returned by the Kerberos client.
#[instrument(ret, level = "trace")]
pub(super) fn decode_initial_neg_init(data: &[u8]) -> crate::Result<(Option<Vec<u8>>, MechTypeList)> {
let token: ApplicationTag0<GssApiNegInit> = picky_asn1_der::from_bytes(data)?;
let NegTokenInit {
mech_types,
req_flags: _,
mech_token,
mech_list_mic: _,
} = token.0.neg_token_init.0;
let mech_types = mech_types
.0
.ok_or_else(|| {
Error::new(
ErrorKind::InvalidToken,
"mech_types is missing in GssApiNegInit message",
)
})?
.0;
let token = mech_token.0.map(|token| token.0.0);
Ok((token, mech_types))
}
/// Selects the preferred authentication protocol OID based on the provided protocols list, allowed protocols,
/// and available protocols.
///
/// The Kerberos protocol will be selected only if it is allowed in the package list, its OID is present in the mech types,
/// and the internal protocol is configured to Kerberos. We cannot _just_ configure it from env vars as we do it for
/// the client-side Kerberos because the server-side Kerberos requires many configuration fields.
///
/// 1.2.840.48018.1.2.2 (MS KRB5 - Microsoft Kerberos 5) is preferred over 1.2.840.113554.1.2.2 (KRB5 - Kerberos 5).
pub(super) fn negotiate_mech_type(
mech_list: &MechTypeList,
negotiate: &mut Negotiate,
) -> crate::Result<(ObjectIdentifier, usize)> {
let ms_krb5 = oids::ms_krb5();
if let Some(mech_index) = mech_list.0.iter().position(|mech_type| mech_type.0 == ms_krb5)
&& negotiate.package_list.kerberos
&& negotiate.protocol.is_kerberos()
{
return Ok((ms_krb5, mech_index));
}
let krb5 = oids::krb5();
if let Some(mech_index) = mech_list.0.iter().position(|mech_type| mech_type.0 == krb5)
&& negotiate.package_list.kerberos
&& negotiate.protocol.is_kerberos()
{
return Ok((krb5, mech_index));
}
let ntlm_oid = oids::ntlm_ssp();
if let Some(mech_index) = mech_list.0.iter().position(|mech_type| mech_type.0 == ntlm_oid)
&& negotiate.package_list.ntlm
{
if let NegotiatedProtocol::Kerberos(kerberos) = &mut negotiate.protocol {
// Negotiate is configured to use Kerberos, but only NTLM is possible (fallback to NTLM).
negotiate.protocol = NegotiatedProtocol::Ntlm(Ntlm::with_config(NtlmConfig {
client_computer_name: Some(kerberos.config.client_computer_name.clone()),
}));
}
return Ok((ntlm_oid, mech_index));
}
Err(Error::new(
ErrorKind::InvalidToken,
"no supported authentication protocols found in mech list",
))
}
+101
View File
@@ -0,0 +1,101 @@
use oid::ObjectIdentifier;
use picky::oids;
use picky_asn1::wrapper::{
Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3,
ObjectIdentifierAsn1, OctetStringAsn1, Optional,
};
use picky_asn1_der::Asn1RawDer;
use picky_krb::constants::gss_api::ACCEPT_INCOMPLETE;
use picky_krb::gss_api::{
ApplicationTag0, GssApiNegInit, MechType, MechTypeList, NegTokenInit, NegTokenTarg, NegTokenTarg1,
};
use crate::{Error, ErrorKind, Result};
/// Generates supported mechanism type list.
pub(super) fn generate_mech_type_list(kerberos: bool, ntlm: bool) -> Result<MechTypeList> {
if !ntlm && !kerberos {
return Err(Error::new(
ErrorKind::InvalidParameter,
"NTLM and Kerberos are not enabled. no security packages available",
));
}
let mut mech_types = Vec::new();
if kerberos {
mech_types.push(MechType::from(oids::ms_krb5()));
mech_types.push(MechType::from(oids::krb5()));
// NEGOEX is not supported.
// mech_types.push(MechType::from(oids::negoex()));
}
if ntlm {
mech_types.push(MechType::from(oids::ntlm_ssp()));
}
Ok(MechTypeList::from(Asn1SequenceOf::from(mech_types)))
}
/// Generates the initial SPNEGO token.
///
/// The `sname` parameter is optional. If it is present, then the Kerberos U2U is in use, and `TgtReq` will be generated
/// for the input `sname` and placed in the `mech_token` field.
pub(super) fn generate_neg_token_init(
mech_list: MechTypeList,
mech_token: Option<Vec<u8>>,
) -> Result<ApplicationTag0<GssApiNegInit>> {
let mech_token = mech_token.map(|token| ExplicitContextTag2::from(OctetStringAsn1::from(token)));
Ok(ApplicationTag0(GssApiNegInit {
oid: ObjectIdentifierAsn1::from(oids::spnego()),
neg_token_init: ExplicitContextTag0::from(NegTokenInit {
mech_types: Optional::from(Some(ExplicitContextTag0::from(mech_list))),
req_flags: Optional::from(None),
mech_token: Optional::from(mech_token),
mech_list_mic: Optional::from(None),
}),
}))
}
pub(super) fn generate_neg_token_targ_1(response_token: Option<Vec<u8>>) -> NegTokenTarg1 {
NegTokenTarg1::from(NegTokenTarg {
neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_INCOMPLETE.to_vec())))),
supported_mech: Optional::from(None),
response_token: Optional::from(
response_token.map(|token| ExplicitContextTag2::from(OctetStringAsn1::from(token))),
),
mech_list_mic: Optional::from(None),
})
}
pub(super) fn generate_final_neg_token_targ(
neg_result: Vec<u8>,
response_token: Option<Vec<u8>>,
mech_list_mic: Option<Vec<u8>>,
) -> NegTokenTarg1 {
NegTokenTarg1::from(NegTokenTarg {
neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(neg_result)))),
supported_mech: Optional::from(None),
response_token: Optional::from(
response_token.map(|token| ExplicitContextTag2::from(OctetStringAsn1::from(token))),
),
mech_list_mic: Optional::from(mech_list_mic.map(|v| ExplicitContextTag3::from(OctetStringAsn1::from(v)))),
})
}
pub(super) fn generate_neg_token_targ(
neg_result: Vec<u8>,
mech_type: ObjectIdentifier,
response_token: Option<Vec<u8>>,
) -> Result<NegTokenTarg1> {
let response_token =
response_token.map(|response_token| ExplicitContextTag2::from(OctetStringAsn1::from(response_token)));
Ok(NegTokenTarg1::from(NegTokenTarg {
neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(neg_result)))),
supported_mech: Optional::from(Some(ExplicitContextTag1::from(MechType::from(mech_type)))),
response_token: Optional::from(response_token),
mech_list_mic: Optional::from(None),
}))
}
+891
View File
@@ -0,0 +1,891 @@
pub(crate) mod client;
mod config;
mod extractors;
mod generators;
pub(crate) mod server;
use std::fmt::Debug;
use std::mem;
use std::net::IpAddr;
use std::sync::LazyLock;
pub use config::{NegotiateConfig, ProtocolConfig};
use picky::oids;
use picky_krb::gss_api::MechType;
use widestring::Utf16String;
use crate::builders::{EmptyAcceptSecurityContext, FilledAcceptSecurityContext, FilledInitializeSecurityContext};
use crate::generator::{
GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal,
};
use crate::kdc::detect_kdc_url;
use crate::kerberos::client::principal::{get_client_principal_name, get_client_principal_realm};
use crate::ntlm::NtlmConfig;
#[allow(unused)]
use crate::utils::is_azure_ad_domain;
use crate::{
AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, BufferType, CertTrustStatus,
ContextNames, ContextSizes, CredentialUse, Credentials, CredentialsBuffers, DecryptionFlags, Error, ErrorKind,
InitializeSecurityContextResult, Kerberos, KerberosConfig, Ntlm, PACKAGE_ID_NONE, PackageCapabilities, PackageInfo,
Pku2u, Result, SecurityBuffer, SecurityBufferRef, SecurityPackageType, SecurityStatus, Sspi, SspiEx, SspiImpl,
builders, kerberos, ntlm, pku2u,
};
pub const PKG_NAME: &str = "Negotiate";
const GUEST_USERNAME: &str = "/GUEST";
pub static PACKAGE_INFO: LazyLock<PackageInfo> = LazyLock::new(|| PackageInfo {
capabilities: PackageCapabilities::empty(),
rpc_id: PACKAGE_ID_NONE,
max_token_len: 0xbb80, // 48 000 bytes: default maximum token len in Windows
name: SecurityPackageType::Negotiate,
comment: String::from("Microsoft Package Negotiator"),
});
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum NegotiatedProtocol {
Pku2u(Pku2u),
Kerberos(Kerberos),
Ntlm(Ntlm),
}
impl NegotiatedProtocol {
pub fn protocol_name(&self) -> &str {
match self {
NegotiatedProtocol::Pku2u(_) => pku2u::PKG_NAME,
NegotiatedProtocol::Kerberos(_) => kerberos::PKG_NAME,
NegotiatedProtocol::Ntlm(_) => ntlm::PKG_NAME,
}
}
pub fn verify_mic_token(&mut self, token: &[u8], data: &[u8], sealed: crate::private::Sealed) -> Result<()> {
match self {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.verify_mic_token(token, data, sealed),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.verify_mic_token(token, data, sealed),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.verify_mic_token(token, data, sealed),
}
}
pub fn generate_mic_token(&mut self, data: &[u8], sealed: crate::private::Sealed) -> Result<Vec<u8>> {
match self {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.generate_mic_token(data, sealed),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.generate_mic_token(data, sealed),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.generate_mic_token(data, sealed),
}
}
pub fn is_kerberos(&self) -> bool {
matches!(self, NegotiatedProtocol::Kerberos(_))
}
pub fn is_ntlm(&self) -> bool {
matches!(self, NegotiatedProtocol::Ntlm(_))
}
async fn initialize_security_context<'a>(
&'a mut self,
auth_identity: Option<&CredentialsBuffers>,
yield_point: &mut YieldPointLocal,
builder: &'a mut FilledInitializeSecurityContext<'_, '_, <Negotiate as SspiImpl>::CredentialsHandle>,
) -> Result<InitializeSecurityContextResult> {
match self {
NegotiatedProtocol::Pku2u(pku2u) => {
let mut credentials_handle = auth_identity.and_then(|c| c.to_auth_identity());
let mut transformed_builder = builder.full_transform(Some(&mut credentials_handle));
let result = pku2u.initialize_security_context_impl(&mut transformed_builder)?;
builder.output = mem::take(&mut transformed_builder.output);
Ok(result)
}
NegotiatedProtocol::Kerberos(kerberos) => {
kerberos.initialize_security_context_impl(yield_point, builder).await
}
NegotiatedProtocol::Ntlm(ntlm) => {
let mut credentials_handle = auth_identity.and_then(|c| c.to_auth_identity());
let mut transformed_builder = builder.full_transform(Some(&mut credentials_handle));
let result = ntlm.initialize_security_context_impl(&mut transformed_builder)?;
builder.output = mem::take(&mut transformed_builder.output);
Ok(result)
}
}
}
async fn accept_security_context(
&mut self,
yield_point: &mut YieldPointLocal,
builder: &mut FilledAcceptSecurityContext<'_, <Negotiate as SspiImpl>::CredentialsHandle>,
) -> Result<AcceptSecurityContextResult> {
let input = builder
.input
.as_mut()
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?;
let mut input_tokens = input.to_vec();
let mut output_tokens = builder.output.to_vec();
let mut creds_handle = builder.credentials_handle.as_ref().and_then(|creds| (*creds).clone());
let result = match self {
NegotiatedProtocol::Pku2u(pku2u) => {
let mut creds_handle = creds_handle.and_then(|creds_handle| creds_handle.into_auth_identity());
let new_builder: FilledAcceptSecurityContext<'_, Option<crate::AuthIdentityBuffers>> =
EmptyAcceptSecurityContext::new()
.with_context_requirements(builder.context_requirements)
.with_target_data_representation(builder.target_data_representation)
.with_input(&mut input_tokens)
.with_output(&mut output_tokens)
.with_credentials_handle(&mut creds_handle);
pku2u.accept_security_context_impl(yield_point, new_builder).await?
}
NegotiatedProtocol::Kerberos(kerberos) => {
let new_builder = EmptyAcceptSecurityContext::new()
.with_context_requirements(builder.context_requirements)
.with_target_data_representation(builder.target_data_representation)
.with_input(&mut input_tokens)
.with_output(&mut output_tokens)
.with_credentials_handle(&mut creds_handle);
kerberos.accept_security_context_impl(yield_point, new_builder).await?
}
NegotiatedProtocol::Ntlm(ntlm) => {
let mut creds_handle = creds_handle.and_then(|creds_handle| creds_handle.into_auth_identity());
let new_builder = EmptyAcceptSecurityContext::new()
.with_credentials_handle(&mut creds_handle)
.with_context_requirements(builder.context_requirements)
.with_target_data_representation(builder.target_data_representation)
.with_input(&mut input_tokens)
.with_output(&mut output_tokens);
ntlm.accept_security_context_impl(new_builder)?
}
};
let output_token = SecurityBuffer::find_buffer_mut(&mut output_tokens, BufferType::Token)?;
let ot = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
ot.buffer = mem::take(&mut output_token.buffer);
Ok(result)
}
fn query_context_names(&mut self) -> Result<ContextNames> {
match self {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_names(),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_names(),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_names(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum NegotiateState {
#[default]
Initial,
InProgress,
VerifyMic,
Ok,
}
#[derive(Clone, Debug, PartialEq)]
enum NegotiateMode {
Client,
Server(Vec<AuthIdentity>),
}
impl NegotiateMode {
/// Returns true if the mode is [NegotiateMode::Client].
fn is_client(&self) -> bool {
self == &NegotiateMode::Client
}
}
#[derive(Clone, Debug)]
pub struct Negotiate {
state: NegotiateState,
protocol: NegotiatedProtocol,
package_list: PackageListConfig,
auth_identity: Option<CredentialsBuffers>,
client_computer_name: String,
mode: NegotiateMode,
/// Encoded [MechTypeList]. Used for `mechListMIC` token verification.
mech_types: Vec<u8>,
mic_verified: bool,
/// Indicates whether `mechListMIC` token verification is needed or not.
///
/// According to [RFC 4178: 5. Processing of mechListMIC](https://www.rfc-editor.org/rfc/rfc4178.html#section-5), the `mechListMIC` is optional:
/// > if the accepted mechanism is the most preferred mechanism of both the initiator and the acceptor,
/// > then the MIC token exchange is OPTIONAL.
mic_needed: bool,
}
#[derive(Clone, Copy, Debug)]
struct PackageListConfig {
ntlm: bool,
kerberos: bool,
pku2u: bool,
}
impl PackageListConfig {
fn parse(package_list: &Option<String>) -> PackageListConfig {
let mut ntlm: bool = true;
let mut kerberos: bool = true;
let mut pku2u: bool = true;
if let Some(package_list) = &package_list {
let mut use_default_packages_configuration = true;
for package in package_list.split(',') {
let (package_name, enabled) = if let Some(package_name) = package.strip_prefix('!') {
(package_name.to_lowercase(), false)
} else {
// If client requested at least one package, then we need to disable the default packages configuration.
if use_default_packages_configuration {
ntlm = false;
kerberos = false;
pku2u = false;
use_default_packages_configuration = false;
}
let package_name = package.to_lowercase();
(package_name, true)
};
match package_name.as_str() {
"ntlm" => ntlm = enabled,
"kerberos" => kerberos = enabled,
"pku2u" => pku2u = enabled,
_ => warn!("unexpected package name: {}", &package_name),
}
}
}
PackageListConfig { ntlm, kerberos, pku2u }
}
}
impl Negotiate {
pub fn new_client(config: NegotiateConfig) -> Result<Self> {
Self::new(config, NegotiateMode::Client)
}
pub fn new_server(config: NegotiateConfig, auth_data: Vec<AuthIdentity>) -> Result<Self> {
Self::new(config, NegotiateMode::Server(auth_data))
}
fn new(config: NegotiateConfig, mode: NegotiateMode) -> Result<Self> {
let mut protocol = config.protocol_config.new_instance()?;
let package_list = PackageListConfig::parse(&config.package_list);
if let Some(filtered_protocol) =
Self::filter_protocol(&protocol, package_list, &config.client_computer_name, mode.is_client())?
{
protocol = filtered_protocol;
}
Ok(Negotiate {
state: Default::default(),
protocol,
package_list,
auth_identity: None,
client_computer_name: config.client_computer_name,
mode,
mech_types: Default::default(),
mic_verified: false,
mic_needed: true,
})
}
#[cfg(feature = "__test-data")]
pub fn mic_needed(&self) -> bool {
self.mic_needed
}
fn protocol_name(&self) -> &str {
self.protocol.protocol_name()
}
fn set_auth_identity(&mut self) -> Result<()> {
let NegotiateMode::Server(auth_data) = &self.mode else {
return Err(Error::new(
ErrorKind::InternalError,
"set_auth_identity must be called only on server side",
));
};
let ContextNames { username } = self.protocol.query_context_names()?;
let candidates: Vec<Credentials> = auth_data
.iter()
.filter(|auth_data| {
trace!("Comparing usernames: {:?} with {:?}", auth_data.username, username);
// Usernames match only when they share the same format and the same components.
// `eq_ignore_ascii_case` is format-aware, so it is safe to fold the UPN suffix and the
// NetBIOS domain into a single "domain" comparison here: a UPN and a down-level logon
// name are distinct identities and never compare equal, which means the qualifier being
// compared always has one unambiguous meaning (a UPN suffix is only ever matched against
// a UPN suffix, a NetBIOS domain only ever against a NetBIOS domain).
auth_data.username.eq_ignore_ascii_case(&username)
})
.cloned()
.map(Credentials::from)
.collect();
if candidates.is_empty() {
return Err(Error::new(
ErrorKind::NoCredentials,
"user credentials are not found on the server side",
));
}
self.custom_set_auth_identities(candidates)
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn negotiate_protocol_by_mech_type(&mut self, mech_type: &MechType, username: Option<&Utf16String>) -> Result<()> {
let enabled_packages = self.package_list;
if mech_type == &oids::ms_krb5() || mech_type == &oids::krb5() {
if !enabled_packages.kerberos {
return Err(Error::new(
ErrorKind::InvalidToken,
"Kerberos mechanism was selected by the server but is disabled in package_list",
));
}
// We disable NTLM completely when the target server has selected Kerberos.
self.package_list.ntlm = false;
if self.protocol_name() != kerberos::PKG_NAME {
let kerberos = Kerberos::new_client_from_config(KerberosConfig {
client_computer_name: self.client_computer_name.clone(),
kdc_url: None,
})?;
self.protocol = NegotiatedProtocol::Kerberos(kerberos);
// When the server changes the protocol from the most preferred for the client to
// any other mechanism type, then `mechListMIC` exchange is required.
//
// [RFC 4178 5. Processing of mechListMIC](https://www.rfc-editor.org/rfc/rfc4178.html#section-5):
// > if the accepted mechanism is the most preferred mechanism of both the initiator and the acceptor,
// > then the MIC token exchange is OPTIONAL.
// > In all other cases, MIC tokens MUST be exchanged after the mechanism context is fully established.
// > ...Note that the MIC token exchange is required if a mechanism other than
// > the initiator's first choice is chosen.
self.mic_needed = true;
self.mic_verified = false;
}
return Ok(());
}
if mech_type == &oids::ntlm_ssp() {
if !enabled_packages.ntlm {
return Err(Error::new(
ErrorKind::InvalidToken,
"NTLM mechanism was selected by the server but is disabled in package_list",
));
}
// We disable Kerberos completely when the target server has selected NTLM.
self.package_list.kerberos = false;
if self.protocol_name() != ntlm::PKG_NAME {
self.protocol =
NegotiatedProtocol::Ntlm(Ntlm::with_config(NtlmConfig::new(self.client_computer_name.clone())));
}
if let Some(user) = username
&& user.to_string().eq_ignore_ascii_case(GUEST_USERNAME)
{
// Do not require `mechListMIC` exchange when the user tries to log on under the guest account.
self.mic_needed = false;
} else {
// [MS-SPNG](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/f377a379-c24f-4a0f-a3eb-0d835389e28a):
// > If NTLM authentication is most preferred by the client and the server, and the client includes a MIC
// > in AUTHENTICATE_MESSAGE ([MS-NLMP] section 2.2.1.3), then the mechListMIC field becomes
// > mandatory in order for the authentication to succeed.
//
// We always include NTLM MIC token inside AUTHENTICATE_MESSAGE. So, we need to perform
// SPNEGO `mechListMIC` exchange.
self.mic_needed = true;
self.mic_verified = false;
}
return Ok(());
}
let s: String = (&mech_type.0).into();
Err(Error::new(
ErrorKind::InvalidToken,
format!("unsupported mech_type: {s}"),
))
}
// negotiates the authorization protocol based on the username and the domain
// Decision rules:
// 1) if `self.protocol` is not NTLM then we've already negotiated a suitable protocol. Nothing to do.
// 2) if the provided domain is Azure AD domain then it'll use Pku2u
// 3) if the provided username is FQDN and we can resolve KDC then it'll use Kerberos
// 4) if SSPI_KDC_URL_ENV is set then it'll also use Kerberos
// 5) in any other cases, it'll use NTLM
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip(self))]
fn negotiate_protocol(&mut self, username: &str, domain: &str) -> Result<()> {
let enabled_packages = self.package_list;
if let NegotiatedProtocol::Ntlm(_) = &self.protocol {
#[cfg(target_os = "windows")]
if enabled_packages.pku2u && is_azure_ad_domain(domain) {
use super::pku2u::Pku2uConfig;
debug!("Negotiate: try Pku2u");
self.protocol = NegotiatedProtocol::Pku2u(Pku2u::new_client_from_config(
Pku2uConfig::default_client_config(self.client_computer_name.clone())?,
)?);
}
if enabled_packages.kerberos
&& let Some(host) = detect_kdc_url(&get_client_principal_realm(username, domain))
{
debug!("Negotiate: try Kerberos");
self.protocol = NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config(KerberosConfig {
kdc_url: Some(host),
client_computer_name: self.client_computer_name.clone(),
})?);
}
}
if let Some(filtered_protocol) = Self::filter_protocol(
&self.protocol,
self.package_list,
&self.client_computer_name,
self.mode.is_client(),
)? {
self.protocol = filtered_protocol;
}
Ok(())
}
fn filter_protocol(
negotiated_protocol: &NegotiatedProtocol,
package_list: PackageListConfig,
client_computer_name: &str,
is_client: bool,
) -> Result<Option<NegotiatedProtocol>> {
let mut filtered_protocol = None;
let PackageListConfig {
ntlm: is_ntlm,
kerberos: is_kerberos,
pku2u: is_pku2u,
} = package_list;
if !is_ntlm && !is_kerberos && !is_pku2u {
return Err(Error::new(
ErrorKind::NoCredentials,
"all security packages are disabled or invalid",
));
}
match &negotiated_protocol {
NegotiatedProtocol::Pku2u(pku2u) => {
if !is_pku2u {
let ntlm_config = NtlmConfig::new(pku2u.config().client_hostname.clone());
filtered_protocol = Some(NegotiatedProtocol::Ntlm(Ntlm::with_config(ntlm_config)));
}
}
NegotiatedProtocol::Kerberos(kerberos) => {
if !is_kerberos {
let ntlm_config = NtlmConfig::new(kerberos.config().client_computer_name.clone());
filtered_protocol = Some(NegotiatedProtocol::Ntlm(Ntlm::with_config(ntlm_config)));
}
}
NegotiatedProtocol::Ntlm(_) => {
if !is_ntlm {
let config = KerberosConfig {
client_computer_name: client_computer_name.to_owned(),
kdc_url: None,
};
if is_client {
let kerberos_client = Kerberos::new_client_from_config(config)?;
filtered_protocol = Some(NegotiatedProtocol::Kerberos(kerberos_client));
} else {
// Aborting because we need an additional data (ServerProperties object) to create the server-side Kerberos instance.
error!(
?package_list,
"NTLM protocol has been negotiated but it is disabled in package_list."
);
return Err(Error::new(
ErrorKind::InternalError,
"NTLM protocol has been negotiated but it is disabled in package_list",
));
}
}
}
}
Ok(filtered_protocol)
}
pub fn negotiated_protocol(&self) -> &NegotiatedProtocol {
&self.protocol
}
fn is_protocol_ntlm(&self) -> bool {
matches!(&self.protocol, NegotiatedProtocol::Ntlm(_))
}
fn can_downgrade_ntlm(&self) -> bool {
self.package_list.ntlm
}
fn is_target_name_ip_address(address: &str) -> bool {
let stripped_address = address.split('/').next_back().unwrap_or(address);
stripped_address.parse::<IpAddr>().is_ok()
}
fn check_target_name_for_ntlm_downgrade(&mut self, target_name: &str) {
let should_downgrade = Self::is_target_name_ip_address(target_name);
let can_downgrade = self.can_downgrade_ntlm();
if can_downgrade && should_downgrade {
// Disable Kerberos and Pku2u when downgrading to NTLM, as they are not suitable because of the target name format.
self.package_list.kerberos = false;
self.package_list.pku2u = false;
if !self.is_protocol_ntlm() {
let ntlm_config = NtlmConfig::new(self.client_computer_name.clone());
self.protocol = NegotiatedProtocol::Ntlm(Ntlm::with_config(ntlm_config));
}
}
}
/// Fallback to NTLM protocol.
///
/// Returns true if the fallback was successful, false if NTLM is disabled and fallback is not possible.
fn fallback_to_ntlm(&mut self) -> bool {
if !self.can_downgrade_ntlm() {
return false;
}
let ntlm_config = NtlmConfig::new(self.client_computer_name.clone());
self.protocol = NegotiatedProtocol::Ntlm(Ntlm::with_config(ntlm_config));
// We need to disable Kerberos completely after falling back to NTLM.
self.package_list.kerberos = false;
true
}
fn verify_mic_token(&mut self, mic: Option<&[u8]>) -> Result<()> {
if let Some(mic) = mic {
self.protocol
.verify_mic_token(mic, &self.mech_types, crate::private::Sealed)?;
self.mic_verified = true;
}
Ok(())
}
}
impl<'a> Negotiate {
pub(crate) async fn accept_security_context_impl(
&'a mut self,
yield_point: &mut YieldPointLocal,
builder: FilledAcceptSecurityContext<'a, <Self as SspiImpl>::CredentialsHandle>,
) -> Result<AcceptSecurityContextResult> {
server::accept_security_context(self, yield_point, builder).await
}
pub(crate) async fn initialize_security_context_impl(
&'a mut self,
yield_point: &mut YieldPointLocal,
builder: &'a mut FilledInitializeSecurityContext<'_, '_, <Self as SspiImpl>::CredentialsHandle>,
) -> Result<InitializeSecurityContextResult> {
client::initialize_security_context(self, yield_point, builder).await
}
}
impl SspiEx for Negotiate {
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn custom_set_auth_identity(&mut self, identity: Self::AuthenticationData) -> Result<()> {
self.auth_identity = Some(identity.clone().try_into().unwrap());
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => {
pku2u.custom_set_auth_identity(identity.auth_identity().ok_or_else(|| {
Error::new(
ErrorKind::IncompleteCredentials,
"Provided credentials are not password-based",
)
})?)
}
NegotiatedProtocol::Kerberos(kerberos) => kerberos.custom_set_auth_identity(identity),
NegotiatedProtocol::Ntlm(ntlm) => {
ntlm.custom_set_auth_identity(identity.auth_identity().ok_or_else(|| {
Error::new(
ErrorKind::IncompleteCredentials,
"Provided credentials are not password-based",
)
})?)
}
}
}
fn custom_set_auth_identities(&mut self, identities: Vec<Self::AuthenticationData>) -> Result<()> {
if let Some(first) = identities.first() {
self.auth_identity = Some(first.clone().try_into().map_err(|_| {
Error::new(
ErrorKind::IncompleteCredentials,
"provided credentials are not password-based",
)
})?);
}
match &mut self.protocol {
NegotiatedProtocol::Ntlm(ntlm) => {
// NOTE: non-AuthIdentity credentials (e.g. SmartCard) are silently
// dropped here. Multi-credential only applies to password-based auth.
let auth_identities: Vec<_> = identities.into_iter().filter_map(|c| c.auth_identity()).collect();
ntlm.custom_set_auth_identities(auth_identities)
}
_ => match identities.into_iter().next() {
Some(identity) => self.custom_set_auth_identity(identity),
None => Err(Error::new(ErrorKind::NoCredentials, "no credentials provided")),
},
}
}
}
impl Sspi for Negotiate {
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip(self))]
fn complete_auth_token(&mut self, token: &mut [SecurityBuffer]) -> Result<SecurityStatus> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.complete_auth_token(token),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.complete_auth_token(token),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.complete_auth_token(token),
}
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn encrypt_message(
&mut self,
flags: crate::EncryptionFlags,
message: &mut [SecurityBufferRef<'_>],
) -> Result<SecurityStatus> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.encrypt_message(flags, message),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.encrypt_message(flags, message),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.encrypt_message(flags, message),
}
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn decrypt_message(&mut self, message: &mut [SecurityBufferRef<'_>]) -> Result<DecryptionFlags> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.decrypt_message(message),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.decrypt_message(message),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.decrypt_message(message),
}
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn query_context_sizes(&mut self) -> Result<ContextSizes> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_sizes(),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_sizes(),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_sizes(),
}
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn query_context_names(&mut self) -> Result<ContextNames> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_names(),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_names(),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_names(),
}
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn query_context_package_info(&mut self) -> Result<PackageInfo> {
crate::query_security_package_info(SecurityPackageType::Negotiate)
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn query_context_negotiation_package(&mut self) -> Result<PackageInfo> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_package_info(),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_package_info(),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_package_info(),
}
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn query_context_cert_trust_status(&mut self) -> Result<CertTrustStatus> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_cert_trust_status(),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_cert_trust_status(),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_cert_trust_status(),
}
}
#[instrument(fields(protocol = self.protocol.protocol_name()), skip_all)]
fn query_context_session_key(&self) -> Result<crate::SessionKeys> {
match &self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_session_key(),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_session_key(),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_session_key(),
}
}
fn change_password<'a>(
&'a mut self,
change_password: builders::ChangePassword<'a>,
) -> Result<GeneratorChangePassword<'a>> {
Ok(GeneratorChangePassword::new(move |mut yield_point| async move {
self.change_password(&mut yield_point, change_password).await
}))
}
fn make_signature(
&mut self,
flags: u32,
message: &mut [SecurityBufferRef<'_>],
sequence_number: u32,
) -> Result<()> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.make_signature(flags, message, sequence_number),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.make_signature(flags, message, sequence_number),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.make_signature(flags, message, sequence_number),
}
}
fn verify_signature(&mut self, message: &mut [SecurityBufferRef<'_>], sequence_number: u32) -> Result<u32> {
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => pku2u.verify_signature(message, sequence_number),
NegotiatedProtocol::Kerberos(kerberos) => kerberos.verify_signature(message, sequence_number),
NegotiatedProtocol::Ntlm(ntlm) => ntlm.verify_signature(message, sequence_number),
}
}
}
impl SspiImpl for Negotiate {
type CredentialsHandle = Option<CredentialsBuffers>;
type AuthenticationData = Credentials;
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn acquire_credentials_handle_impl(
&mut self,
builder: builders::FilledAcquireCredentialsHandle<'_, Self::CredentialsHandle, Self::AuthenticationData>,
) -> Result<AcquireCredentialsHandleResult<Self::CredentialsHandle>> {
if builder.credential_use == CredentialUse::Outbound && builder.auth_data.is_none() {
return Err(Error::new(
ErrorKind::NoCredentials,
"The client must specify the auth data",
));
}
if let Some(Credentials::AuthIdentity(identity)) = builder.auth_data {
let account_name = identity.username.account_name();
// `realm_domain` is the per-format "authority" (UPN suffix or NetBIOS domain) used purely
// as a best-effort realm/Azure-AD hint for protocol negotiation, not as an identity.
let domain_name = get_client_principal_name(&identity.username).realm_domain;
self.negotiate_protocol(account_name, domain_name)?;
}
self.auth_identity = builder
.auth_data
.cloned()
.map(|auth_data| auth_data.try_into())
.transpose()?;
match &mut self.protocol {
NegotiatedProtocol::Pku2u(pku2u) => {
let auth_identity = if let Some(Credentials::AuthIdentity(identity)) = builder.auth_data {
identity
} else {
return Err(Error::new(
ErrorKind::NoCredentials,
"Auth identity is not provided for the Pku2u",
));
};
let new_builder = builder.full_transform(Some(auth_identity));
new_builder.execute(pku2u)?;
}
NegotiatedProtocol::Kerberos(kerberos) => {
kerberos.acquire_credentials_handle_impl(builder)?;
}
NegotiatedProtocol::Ntlm(ntlm) => {
let auth_identity = if builder.credential_use == CredentialUse::Outbound {
if let Some(Credentials::AuthIdentity(identity)) = builder.auth_data {
Some(identity)
} else {
return Err(Error::new(
ErrorKind::NoCredentials,
"Auth identity is not provided for the Ntlm",
));
}
} else {
None
};
let new_builder = builder.full_transform(auth_identity);
new_builder.execute(ntlm)?;
}
};
Ok(AcquireCredentialsHandleResult {
credentials_handle: self.auth_identity.clone(),
expiry: None,
})
}
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
fn accept_security_context_impl<'a>(
&'a mut self,
builder: FilledAcceptSecurityContext<'a, Self::CredentialsHandle>,
) -> Result<GeneratorAcceptSecurityContext<'a>> {
Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move {
server::accept_security_context(self, &mut yield_point, builder).await
}))
}
fn initialize_security_context_impl<'ctx, 'b, 'g>(
&'ctx mut self,
builder: &'b mut FilledInitializeSecurityContext<'ctx, 'ctx, Self::CredentialsHandle>,
) -> Result<GeneratorInitSecurityContext<'g>>
where
'ctx: 'g,
'b: 'g,
{
Ok(GeneratorInitSecurityContext::new(move |mut yield_point| async move {
client::initialize_security_context(self, &mut yield_point, builder).await
}))
}
}
impl<'a> Negotiate {
#[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)]
pub(crate) async fn change_password(
&'a mut self,
yield_point: &mut YieldPointLocal,
change_password: builders::ChangePassword<'a>,
) -> Result<()> {
self.negotiate_protocol(&change_password.account_name, &change_password.domain_name)?;
match &mut self.protocol {
NegotiatedProtocol::Kerberos(kerberos) => kerberos.change_password(yield_point, change_password).await,
_ => Err(Error::new(
ErrorKind::UnsupportedFunction,
"cannot change password for this protocol",
)),
}
}
}
+270
View File
@@ -0,0 +1,270 @@
use std::mem;
use picky_krb::constants::gss_api::{ACCEPT_COMPLETE, ACCEPT_INCOMPLETE};
use picky_krb::gss_api::{NegTokenTarg, NegTokenTarg1};
use crate::builders::FilledAcceptSecurityContext;
use crate::generator::YieldPointLocal;
use crate::negotiate::extractors::{decode_initial_neg_init, negotiate_mech_type};
use crate::negotiate::generators::{generate_final_neg_token_targ, generate_neg_token_targ, generate_neg_token_targ_1};
use crate::negotiate::{GUEST_USERNAME, NegotiateState};
use crate::{
AcceptSecurityContextResult, BufferType, ContextNames, Error, ErrorKind, Negotiate, NegotiatedProtocol, Result,
SecurityBuffer, SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl,
};
/// Performs one authentication step.
///
/// The user should call this function until it returns `SecurityStatus::Ok`.
#[instrument(ret, fields(protocol = negotiate.protocol_name()), skip_all)]
pub(crate) async fn accept_security_context(
negotiate: &mut Negotiate,
yield_point: &mut YieldPointLocal,
mut builder: FilledAcceptSecurityContext<'_, <Negotiate as SspiImpl>::CredentialsHandle>,
) -> Result<AcceptSecurityContextResult> {
let input = builder
.input
.as_mut()
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?;
let input_token = SecurityBuffer::find_buffer_mut(input, BufferType::Token)?;
let status = match negotiate.state {
NegotiateState::Initial => {
let (mech_token, mech_types) = decode_initial_neg_init(&input_token.buffer)?;
let (mech_type, mech_index) = negotiate_mech_type(&mech_types, negotiate)?;
negotiate.mech_types = picky_asn1_der::to_vec(&mech_types)?;
let mut status = SecurityStatus::ContinueNeeded;
let encoded_neg_token_targ = if mech_index != 0 {
// The selected mech type is not the most preferred one by client, so MIC token exchange is required according to RFC 4178.
//
// [RFC 4178 5. Processing of mechListMIC](https://www.rfc-editor.org/rfc/rfc4178.html#section-5):
// > if the accepted mechanism is the most preferred mechanism of both the initiator and the acceptor,
// > then the MIC token exchange is OPTIONAL.
// > In all other cases, MIC tokens MUST be exchanged after the mechanism context is fully established.
// > ...Note that the MIC token exchange is required if a mechanism other than
// > the initiator's first choice is chosen.
negotiate.mic_needed = true;
negotiate.mic_verified = false;
negotiate.state = NegotiateState::InProgress;
// The selected mech type is not the most preferred one by client, so we cannot use the token sent by the client.
picky_asn1_der::to_vec(&generate_neg_token_targ(ACCEPT_INCOMPLETE.to_vec(), mech_type, None)?)?
} else {
// The selected mech type is the most preferred one by client.
if negotiate.protocol.is_ntlm() {
// [MS-SPNG](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/f377a379-c24f-4a0f-a3eb-0d835389e28a):
// > If NTLM authentication is most preferred by the client and the server, and the client includes a MIC
// > in AUTHENTICATE_MESSAGE ([MS-NLMP] section 2.2.1.3), then the mechListMIC field becomes
// > mandatory in order for the authentication to succeed.
//
// We always include NTLM MIC token inside AUTHENTICATE_MESSAGE. So, we need to perform
// SPNEGO `mechListMIC` exchange.
negotiate.mic_needed = true;
negotiate.mic_verified = false;
} else {
// So, MIC exchange is not needed and we can use the token sent by the client.
negotiate.mic_needed = false;
}
let (response_token, neg_result) = if let Some(mut mech_token) = mech_token {
input_token.buffer = mem::take(&mut mech_token);
let result = negotiate
.protocol
.accept_security_context(yield_point, &mut builder)
.await?;
let neg_result =
if result.status == SecurityStatus::Ok || result.status == SecurityStatus::CompleteNeeded {
if !negotiate.mic_needed || negotiate.mic_verified {
negotiate.state = NegotiateState::Ok;
status = SecurityStatus::Ok;
ACCEPT_COMPLETE
} else {
negotiate.state = NegotiateState::VerifyMic;
ACCEPT_INCOMPLETE
}
} else {
negotiate.state = NegotiateState::InProgress;
ACCEPT_INCOMPLETE
};
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
(Some(mem::take(&mut output_token.buffer)), neg_result)
} else {
(None, ACCEPT_INCOMPLETE)
};
picky_asn1_der::to_vec(&generate_neg_token_targ(
neg_result.to_vec(),
mech_type,
response_token,
)?)?
};
let is_kerberos_u2u = if let NegotiatedProtocol::Kerberos(kerberos) = &negotiate.protocol {
kerberos.krb5_user_to_user
} else {
false
};
if is_kerberos_u2u || builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
negotiate.mic_needed = true;
negotiate.mic_verified = false;
}
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
output_token.buffer = encoded_neg_token_targ;
status
}
NegotiateState::InProgress => {
let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?;
let NegTokenTarg {
neg_result,
supported_mech: _,
response_token,
mech_list_mic,
} = neg_token_targ.0;
let input_token = SecurityBuffer::find_buffer_mut(input, BufferType::Token)?;
let token = response_token.0.map(|token| token.0.0);
if let Some(token) = token {
input_token.buffer = token;
} else {
input_token.buffer.clear();
}
let mut result = negotiate
.protocol
.accept_security_context(yield_point, &mut builder)
.await?;
if result.status == SecurityStatus::Ok || result.status == SecurityStatus::CompleteNeeded {
let mech_list_mic = mech_list_mic.0.map(|token| token.0.0);
if mech_list_mic.is_some() && negotiate.mic_needed {
negotiate.set_auth_identity()?;
negotiate.verify_mic_token(mech_list_mic.as_deref())?;
negotiate.mic_verified = true;
}
if negotiate.mic_needed
&& mech_list_mic.is_none()
&& neg_result.0.as_ref().map(|neg_result| neg_result.0.0.as_slice()) == Some(&ACCEPT_COMPLETE)
{
// We should skip `mechListMIC` exchange when the client tries guest logon.
let ContextNames { username } = negotiate.protocol.query_context_names()?;
if !username.inner().eq_ignore_ascii_case(GUEST_USERNAME) {
return Err(Error::new(
ErrorKind::InvalidToken,
"the client skipped `mechListMIC` exchange, but it is required for non-guest logon",
));
}
negotiate.mic_needed = false;
}
let neg_result = if !negotiate.mic_needed || negotiate.mic_verified {
negotiate.state = NegotiateState::Ok;
result.status = SecurityStatus::Ok;
ACCEPT_COMPLETE.to_vec()
} else {
negotiate.state = NegotiateState::VerifyMic;
result.status = SecurityStatus::ContinueNeeded;
ACCEPT_INCOMPLETE.to_vec()
};
prepare_neg_token(neg_result, negotiate, &mut builder)?;
} else {
// Wrap in a NegToken.
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
let spnego_token =
picky_asn1_der::to_vec(&generate_neg_token_targ_1(Some(mem::take(&mut output_token.buffer))))?;
output_token.buffer = spnego_token;
}
result.status
}
NegotiateState::VerifyMic => {
if !negotiate.mic_verified && negotiate.mic_needed {
let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?;
let NegTokenTarg {
neg_result: _,
supported_mech: _,
response_token: _,
mech_list_mic,
} = neg_token_targ.0;
let mech_list_mic = mech_list_mic.0.map(|token| token.0.0);
if mech_list_mic.is_some() {
negotiate.set_auth_identity()?;
negotiate.verify_mic_token(mech_list_mic.as_deref())?;
} else {
return Err(Error::new(
ErrorKind::InvalidToken,
"mech_list_mic is not present in SPNEGO message",
));
}
}
SecurityStatus::Ok
}
NegotiateState::Ok => {
return Err(Error::new(
ErrorKind::OutOfSequence,
"accept_security_context called after negotiation completed",
));
}
};
Ok(AcceptSecurityContextResult {
status,
flags: ServerResponseFlags::empty(),
expiry: None,
})
}
fn prepare_neg_token(
neg_result: Vec<u8>,
negotiate: &mut Negotiate,
builder: &mut FilledAcceptSecurityContext<'_, <Negotiate as SspiImpl>::CredentialsHandle>,
) -> Result<()> {
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
let response_token = if !output_token.buffer.is_empty() {
Some(mem::take(&mut output_token.buffer))
} else {
None
};
let mic = if negotiate.mic_needed {
Some(
negotiate
.protocol
.generate_mic_token(&negotiate.mech_types, crate::private::Sealed)?,
)
} else {
None
};
let neg_token_targ = generate_final_neg_token_targ(neg_result, response_token, mic);
let encoded_final_neg_token_targ = picky_asn1_der::to_vec(&neg_token_targ)?;
output_token.buffer = encoded_final_neg_token_targ;
Ok(())
}