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:
+329
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user