diff --git a/src/credssp/mod.rs b/src/credssp/mod.rs index a69e80c6..d3261f7e 100644 --- a/src/credssp/mod.rs +++ b/src/credssp/mod.rs @@ -383,7 +383,7 @@ impl CredSspClient { let pub_key_auth = ts_request.pub_key_auth.take().ok_or_else(|| { Error::new( ErrorKind::InvalidToken, - String::from("expected an encrypted public key"), + String::from("CredSspi client expected an encrypted public key"), ) })?; let peer_version = self @@ -634,7 +634,7 @@ impl + Send> CredSspServe ts_request.pub_key_auth.take().ok_or_else(|| { Error::new( ErrorKind::InvalidToken, - String::from("expected an encrypted public key"), + String::from("CredSsp server expected an encrypted public key"), ) }), ts_request diff --git a/src/kerberos/client/generators.rs b/src/kerberos/client/generators.rs index 81d3c24f..43dbb844 100644 --- a/src/kerberos/client/generators.rs +++ b/src/kerberos/client/generators.rs @@ -20,7 +20,7 @@ use picky_krb::constants::key_usages::{ use picky_krb::constants::types::{ AD_AUTH_DATA_AP_OPTION_TYPE, AP_REP_MSG_TYPE, AP_REQ_MSG_TYPE, AS_REQ_MSG_TYPE, KERB_AP_OPTIONS_CBT, KRB_PRIV, NET_BIOS_ADDR_TYPE, NT_ENTERPRISE, NT_PRINCIPAL, NT_SRV_INST, PA_ENC_TIMESTAMP, PA_ENC_TIMESTAMP_KEY_USAGE, - PA_PAC_OPTIONS_TYPE, PA_PAC_REQUEST_TYPE, PA_TGS_REQ_TYPE, TGS_REQ_MSG_TYPE, + PA_PAC_OPTIONS_TYPE, PA_PAC_REQUEST_TYPE, PA_TGS_REQ_TYPE, TGS_REQ_MSG_TYPE, TGT_REQ_MSG_TYPE, }; use picky_krb::crypto::CipherSuite; use picky_krb::data_types::{ @@ -32,7 +32,7 @@ use picky_krb::data_types::{ use picky_krb::gss_api::{MechType, MechTypeList}; use picky_krb::messages::{ ApMessage, ApRep, ApRepInner, ApReq, ApReqInner, AsReq, KdcRep, KdcReq, KdcReqBody, KrbPriv, KrbPrivInner, - KrbPrivMessage, TgsReq, + KrbPrivMessage, TgsReq, TgtReq, }; use rand::rngs::{StdRng, SysRng}; use rand::{RngCore, SeedableRng}; @@ -153,6 +153,22 @@ fn matches_domain(domain: &str, mapping_domain: &str) -> bool { } } +pub(super) fn generate_tgt_req(sname: &[&str]) -> Result { + let sname = sname + .iter() + .map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string(sname.to_string())?))) + .collect::>>()?; + + Ok(TgtReq { + pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), + msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REQ_MSG_TYPE])), + server_name: ExplicitContextTag2::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(sname)), + }), + }) +} + /// Parameters for generating pa-datas for [AsReq] message. #[derive(Debug)] pub struct GenerateAsPaDataOptions<'a> { diff --git a/src/kerberos/client/mod.rs b/src/kerberos/client/mod.rs index 9f9dda64..656561b2 100644 --- a/src/kerberos/client/mod.rs +++ b/src/kerberos/client/mod.rs @@ -8,7 +8,7 @@ use std::io::Write; pub(crate) use as_exchange::as_exchange; pub use change_password::change_password; use picky_asn1_x509::oids; -use picky_krb::constants::gss_api::{AP_REP_TOKEN_ID, AP_REQ_TOKEN_ID, AUTHENTICATOR_CHECKSUM_TYPE}; +use picky_krb::constants::gss_api::{AP_REP_TOKEN_ID, AP_REQ_TOKEN_ID, AUTHENTICATOR_CHECKSUM_TYPE, TGT_REQ_TOKEN_ID}; use picky_krb::crypto::CipherSuite; use picky_krb::data_types::{KrbResult, ResultExt}; use picky_krb::messages::{ApRep, TgsRep}; @@ -27,11 +27,12 @@ use self::generators::{ }; use crate::channel_bindings::ChannelBindings; use crate::generator::YieldPointLocal; +use crate::kerberos::client::generators::generate_tgt_req; use crate::kerberos::messages::{decode_krb_message, generate_krb_message}; use crate::kerberos::pa_datas::{AsRepSessionKeyExtractor, AsReqPaDataOptions}; use crate::kerberos::utils::serialize_message; use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EC, TGT_SERVICE_NAME}; -use crate::utils::generate_random_symmetric_key; +use crate::utils::{generate_random_symmetric_key, parse_target_name}; use crate::{ BufferType, ClientRequestFlags, ClientResponseFlags, CredentialsBuffers, Error, ErrorKind, InitializeSecurityContextResult, Kerberos, KerberosState, Result, SecurityBuffer, SecurityStatus, SspiImpl, @@ -51,6 +52,47 @@ pub async fn initialize_security_context<'a>( ) -> Result { trace!(?builder); + if let KerberosState::TgtExchange = client.state { + if builder + .context_requirements + .contains(ClientRequestFlags::USE_SESSION_KEY) + { + client.krb5_user_to_user = true; + + let (service_name, service_principal_name) = parse_target_name(builder.target_name.ok_or_else(|| { + Error::new( + ErrorKind::NoCredentials, + "Service target name (service principal name) is not provided", + ) + })?)?; + + let tgt_req = generate_tgt_req(&[service_name, service_principal_name])?; + + let encoded_neg_tgt_req = if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { + generate_krb_message(oids::krb5_user_to_user(), TGT_REQ_TOKEN_ID, tgt_req)? + } else { + // Do not wrap if the `USE_DCE_STYLE` flag is set. + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 + picky_asn1_der::to_vec(&tgt_req)? + }; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer = encoded_neg_tgt_req; + + client.state = KerberosState::Preauthentication; + + trace!(output_buffers = ?builder.output); + + return Ok(InitializeSecurityContextResult { + status: SecurityStatus::ContinueNeeded, + flags: ClientResponseFlags::empty(), + expiry: None, + }); + } else { + client.state = KerberosState::Preauthentication; + } + } + let status = match client.state { KerberosState::Preauthentication => { let input = builder @@ -310,14 +352,12 @@ pub async fn initialize_security_context<'a>( context_requirements.into(), )?; - let encoded_ap_req = picky_asn1_der::to_vec(&ap_req)?; - let encoded_neg_ap_req = if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { generate_krb_message(mech_id, AP_REQ_TOKEN_ID, ap_req)? } else { // Do not wrap if the `USE_DCE_STYLE` flag is set. // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 - encoded_ap_req + picky_asn1_der::to_vec(&ap_req)? }; let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; @@ -380,7 +420,7 @@ pub async fn initialize_security_context<'a>( client.state = KerberosState::Final; SecurityStatus::Ok } - KerberosState::Final => { + KerberosState::Final | KerberosState::TgtExchange => { return Err(Error::new( ErrorKind::OutOfSequence, format!("got wrong Kerberos state: {:?}", client.state), diff --git a/src/kerberos/mod.rs b/src/kerberos/mod.rs index 2a73b666..02d4bdbe 100644 --- a/src/kerberos/mod.rs +++ b/src/kerberos/mod.rs @@ -78,6 +78,7 @@ pub static PACKAGE_INFO: LazyLock = LazyLock::new(|| PackageInfo { #[derive(Debug, Clone, Copy, PartialEq)] pub enum KerberosState { + TgtExchange, Preauthentication, ApExchange, Final, @@ -105,7 +106,7 @@ impl Kerberos { let mut rand = StdRng::try_from_rng(&mut SysRng)?; Ok(Self { - state: KerberosState::Preauthentication, + state: KerberosState::TgtExchange, config, auth_identity: None, encryption_params: EncryptionParams::default_for_client(), @@ -125,7 +126,7 @@ impl Kerberos { let mut rand = StdRng::try_from_rng(&mut SysRng)?; Ok(Self { - state: KerberosState::Preauthentication, + state: KerberosState::TgtExchange, config, auth_identity: None, encryption_params: EncryptionParams::default_for_server(), diff --git a/src/kerberos/server/generators.rs b/src/kerberos/server/generators.rs index 72789e49..a831f54e 100644 --- a/src/kerberos/server/generators.rs +++ b/src/kerberos/server/generators.rs @@ -3,11 +3,11 @@ use picky_asn1::wrapper::{ Optional, }; use picky_krb::constants::key_usages::AP_REP_ENC; -use picky_krb::constants::types::AP_REP_MSG_TYPE; +use picky_krb::constants::types::{AP_REP_MSG_TYPE, TGT_REP_MSG_TYPE}; use picky_krb::data_types::{ - EncApRepPart, EncApRepPartInner, EncryptedData, EncryptionKey, KerberosTime, Microseconds, + EncApRepPart, EncApRepPartInner, EncryptedData, EncryptionKey, KerberosTime, Microseconds, Ticket, }; -use picky_krb::messages::{ApRep, ApRepInner}; +use picky_krb::messages::{ApRep, ApRepInner, TgtRep}; use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EncryptionParams}; use crate::{KERBEROS_VERSION, Result, Secret}; @@ -46,3 +46,11 @@ pub(super) fn generate_ap_rep( }), })) } + +pub(super) fn generate_tgt_rep(ticket: Ticket) -> TgtRep { + TgtRep { + pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), + msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REP_MSG_TYPE])), + ticket: ExplicitContextTag2::from(ticket), + } +} diff --git a/src/kerberos/server/mod.rs b/src/kerberos/server/mod.rs index e62643c3..a3bd8661 100644 --- a/src/kerberos/server/mod.rs +++ b/src/kerberos/server/mod.rs @@ -9,11 +9,11 @@ use cache::AuthenticatorCacheRecord; use picky::oids; use picky_asn1::restricted_string::IA5String; use picky_asn1::wrapper::{Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, IntegerAsn1}; -use picky_krb::constants::gss_api::{AP_REP_TOKEN_ID, AP_REQ_TOKEN_ID}; +use picky_krb::constants::gss_api::{AP_REP_TOKEN_ID, AP_REQ_TOKEN_ID, TGT_REP_TOKEN_ID, TGT_REQ_TOKEN_ID}; use picky_krb::constants::types::NT_SRV_INST; use picky_krb::data_types::{AuthenticatorInner, KerberosStringAsn1, PrincipalName}; use picky_krb::gss_api::MechTypeList; -use picky_krb::messages::{ApRep, ApReq}; +use picky_krb::messages::{ApRep, ApReq, TgtReq}; use rand::rngs::{StdRng, SysRng}; use rand::{RngCore, SeedableRng}; use time::OffsetDateTime; @@ -27,7 +27,9 @@ use crate::kerberos::DEFAULT_ENCRYPTION_TYPE; use crate::kerberos::client::extractors::extract_seq_number_from_ap_rep; use crate::kerberos::flags::ApOptions; use crate::kerberos::messages::{decode_krb_message, generate_krb_message}; +use crate::kerberos::server::as_exchange::request_tgt; use crate::kerberos::server::extractors::client_upn; +use crate::kerberos::server::generators::generate_tgt_rep; use crate::{ AcceptSecurityContextResult, BufferType, CredentialsBuffers, Error, ErrorKind, Kerberos, KerberosState, Result, Secret, SecurityBuffer, SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, Username, @@ -98,7 +100,7 @@ impl ServerProperties { /// The user should call this function until it returns `SecurityStatus::Ok`. pub async fn accept_security_context( server: &mut Kerberos, - _yield_point: &mut YieldPointLocal, + yield_point: &mut YieldPointLocal, builder: FilledAcceptSecurityContext<'_, ::CredentialsHandle>, ) -> Result { let input = builder @@ -107,6 +109,75 @@ pub async fn accept_security_context( .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?; let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; + if server.state == KerberosState::TgtExchange { + if let Ok(tgt_req) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { + picky_asn1_der::from_bytes::(&input_token.buffer).map_err(Error::from) + } else { + decode_krb_message::(&input_token.buffer, TGT_REQ_TOKEN_ID) + } { + // The first token is TGT_REQ. It means that the client wants to perform Kerberos U2U. + + if !builder + .context_requirements + .contains(ServerRequestFlags::USE_SESSION_KEY) + { + warn!( + "KRB5 U2U has been negotiated (requested by the client) but the USE_SESSION_KEY flag is not set." + ); + } + + server.krb5_user_to_user = true; + + let credentials = server + .server + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "Kerberos server configuration not present"))? + .user + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "KRB5 U2U has been negotiated (requested by the client) but the user credentials are not preset in Kerberos server configuration"))? + .clone(); + + let tgt_rep = generate_tgt_rep(request_tgt(server, &credentials, &tgt_req, yield_point).await?); + + let mech_id = if server.krb5_user_to_user { + oids::krb5_user_to_user() + } else { + oids::krb5() + }; + + let encoded_tgt_rep = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { + picky_asn1_der::to_vec(&tgt_rep)? + } else { + generate_krb_message(mech_id, TGT_REP_TOKEN_ID, tgt_rep)? + }; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer = encoded_tgt_rep; + + server.state = KerberosState::Preauthentication; + + return Ok(AcceptSecurityContextResult { + status: SecurityStatus::ContinueNeeded, + flags: ServerResponseFlags::empty(), + expiry: None, + }); + } else if let Ok(_ap_req) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { + picky_asn1_der::from_bytes::(&input_token.buffer).map_err(Error::from) + } else { + decode_krb_message::(&input_token.buffer, AP_REQ_TOKEN_ID) + } { + // The client may send ApReq instead of TgtReq in the first message. + // It means that the client wants to perform regular Kerberos without U2U. + // In that case, we just move Kerberos state to the next one and process further. + server.state = KerberosState::Preauthentication; + } else { + return Err(Error::new( + ErrorKind::InvalidToken, + "invalid Kerberos token: expected TgtReq or ApReq", + )); + } + } + let status = match server.state { KerberosState::Preauthentication => { let ap_req = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { @@ -353,7 +424,7 @@ pub async fn accept_security_context( SecurityStatus::Ok } - KerberosState::Final => { + KerberosState::Final | KerberosState::TgtExchange => { return Err(Error::new( ErrorKind::OutOfSequence, format!("got wrong Kerberos state: {:?}", server.state), diff --git a/src/lib.rs b/src/lib.rs index 8ec607ea..bc16099f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,6 +116,8 @@ use self::builders::{ pub use self::kdc::{detect_kdc_host, detect_kdc_url}; pub use self::kerberos::config::{KerberosConfig, KerberosServerConfig}; pub use self::kerberos::{KERBEROS_VERSION, Kerberos, KerberosState}; +#[cfg(feature = "__test-data")] +pub use self::negotiate::client::FALLBACK_ERROR_KINDS; pub use self::negotiate::{Negotiate, NegotiateConfig, NegotiatedProtocol}; pub use self::ntlm::Ntlm; pub use self::ntlm::hash::{NTLM_HASH_PREFIX, NtlmHash, NtlmHashError}; diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index eedf5215..18bb7bb2 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -8,12 +8,29 @@ 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::utils::parse_target_name; 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`. @@ -67,24 +84,69 @@ pub(crate) async fn initialize_security_context<'a>( match negotiate.state { NegotiateState::Initial => { - let sname = if builder + if builder .context_requirements .contains(ClientRequestFlags::USE_SESSION_KEY) + || builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { - let (service_name, service_principal_name) = - parse_target_name(builder.target_name.ok_or_else(|| { - Error::new( - ErrorKind::NoCredentials, - "Service target name (service principal name) is not provided", - ) - })?)?; - - Some([service_name, service_principal_name]) + negotiate.mic_needed = true; + negotiate.mic_verified = false; } else { - None - }; + negotiate.mic_needed = false; + negotiate.mic_verified = true; + } + + 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), + )); + } - debug!(?sname); + 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(_)), @@ -93,10 +155,7 @@ pub(crate) async fn initialize_security_context<'a>( negotiate.mech_types = picky_asn1_der::to_vec(&mech_types)?; - let encoded_neg_token_init = picky_asn1_der::to_vec(&generate_neg_token_init( - sname.as_ref().map(|sname| sname.as_slice()), - 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; @@ -118,23 +177,16 @@ pub(crate) async fn initialize_security_context<'a>( 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, + neg_result: server_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_INCOMPLETE) { - return Err(Error::new( - ErrorKind::InvalidToken, - format!("unexpected NegResult: {neg_result:?}. expected ACCEPT_INCOMPLETE({ACCEPT_INCOMPLETE:?})"), - )); - } - if let Some(selected_mech) = supported_mech.0 { let selected_mech = &selected_mech.0; - debug!("The remote server has selected {selected_mech:?} mechanism id."); + 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)?; } @@ -147,37 +199,18 @@ pub(crate) async fn initialize_security_context<'a>( input_token.buffer.clear(); } - let mut result = match &mut negotiate.protocol { - NegotiatedProtocol::Pku2u(pku2u) => { - let mut credentials_handle = negotiate.auth_identity.as_ref().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); - - result - } - NegotiatedProtocol::Kerberos(kerberos) => { - kerberos.initialize_security_context_impl(yield_point, builder).await? - } - NegotiatedProtocol::Ntlm(ntlm) => { - let mut credentials_handle = negotiate.auth_identity.as_ref().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); - - result - } - }; + let mut result = negotiate + .protocol + .initialize_security_context(negotiate.auth_identity.as_ref(), yield_point, builder) + .await?; if result.status == SecurityStatus::Ok { - let mech_list_mic = mech_list_mic.0.map(|token| token.0.0); - negotiate.verify_mic_token(mech_list_mic.as_deref())?; + 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_verified { + let neg_result = if !negotiate.mic_needed || negotiate.mic_verified { result.status = SecurityStatus::Ok; negotiate.state = NegotiateState::Ok; @@ -189,7 +222,16 @@ pub(crate) async fn initialize_security_context<'a>( ACCEPT_INCOMPLETE.to_vec() }; - prepare_final_neg_token(neg_result, negotiate, builder)?; + 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)?; @@ -247,7 +289,7 @@ pub(crate) async fn initialize_security_context<'a>( } } -fn prepare_final_neg_token( +fn prepare_neg_token( neg_result: Vec, negotiate: &mut Negotiate, builder: &mut crate::builders::FilledInitializeSecurityContext<'_, '_, ::CredentialsHandle>, @@ -260,18 +302,18 @@ fn prepare_final_neg_token( None }; - let neg_token_targ = generate_final_neg_token_targ( - neg_result, - response_token, + 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(()) diff --git a/src/negotiate/extractors.rs b/src/negotiate/extractors.rs index 08dbe978..9c4bfb95 100644 --- a/src/negotiate/extractors.rs +++ b/src/negotiate/extractors.rs @@ -1,15 +1,13 @@ use oid::ObjectIdentifier; use picky::oids; -use picky_krb::gss_api::{ApplicationTag0, GssApiNegInit, KrbMessage, MechTypeList, NegTokenInit}; -use picky_krb::messages::TgtReq; +use picky_krb::gss_api::{ApplicationTag0, GssApiNegInit, MechTypeList, NegTokenInit}; -use crate::negotiate::PackageListConfig; use crate::ntlm::NtlmConfig; -use crate::{Error, ErrorKind, NegotiatedProtocol, Ntlm}; +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, MechTypeList)> { +pub(super) fn decode_initial_neg_init(data: &[u8]) -> crate::Result<(Option>, MechTypeList)> { let token: ApplicationTag0 = picky_asn1_der::from_bytes(data)?; let NegTokenInit { mech_types, @@ -28,28 +26,9 @@ pub(super) fn decode_initial_neg_init(data: &[u8]) -> crate::Result<(Option::decode_application_krb_message(&encoded_tgt_req)?; + let token = mech_token.0.map(|token| token.0.0); - let token_oid = &neg_token_init.0.krb5_oid.0; - let krb5_u2u = oids::krb5_user_to_user(); - if *token_oid != krb5_u2u { - return Err(Error::new( - ErrorKind::InvalidToken, - format!( - "invalid oid inside mech_token: expected krb5 u2u ({:?}) but got {:?}", - krb5_u2u, token_oid - ), - )); - } - - Some(neg_token_init.0.krb_msg) - } else { - None - }; - - Ok((tgt_req, mech_types)) + Ok((token, mech_types)) } /// Selects the preferred authentication protocol OID based on the provided protocols list, allowed protocols, @@ -62,35 +41,36 @@ pub(super) fn decode_initial_neg_init(data: &[u8]) -> crate::Result<(Option crate::Result { + negotiate: &mut Negotiate, +) -> crate::Result<(ObjectIdentifier, usize)> { let ms_krb5 = oids::ms_krb5(); - if mech_list.0.iter().any(|mech_type| mech_type.0 == ms_krb5) - && package_list.kerberos - && internal_protocol.is_kerberos() + 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); + return Ok((ms_krb5, mech_index)); } let krb5 = oids::krb5(); - if mech_list.0.iter().any(|mech_type| mech_type.0 == krb5) - && package_list.kerberos - && internal_protocol.is_kerberos() + 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); + return Ok((krb5, mech_index)); } let ntlm_oid = oids::ntlm_ssp(); - if mech_list.0.iter().any(|mech_type| mech_type.0 == ntlm_oid) && package_list.ntlm { - if let NegotiatedProtocol::Kerberos(kerberos) = internal_protocol { + 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). - *internal_protocol = NegotiatedProtocol::Ntlm(Ntlm::with_config(NtlmConfig { + negotiate.protocol = NegotiatedProtocol::Ntlm(Ntlm::with_config(NtlmConfig { client_computer_name: Some(kerberos.config.client_computer_name.clone()), })); } - return Ok(ntlm_oid); + return Ok((ntlm_oid, mech_index)); } Err(Error::new( diff --git a/src/negotiate/generators.rs b/src/negotiate/generators.rs index d35c9b07..8e77de96 100644 --- a/src/negotiate/generators.rs +++ b/src/negotiate/generators.rs @@ -1,20 +1,16 @@ use oid::ObjectIdentifier; use picky::oids; -use picky_asn1::restricted_string::IA5String; use picky_asn1::wrapper::{ - Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, IntegerAsn1, + Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, ObjectIdentifierAsn1, OctetStringAsn1, Optional, }; use picky_asn1_der::Asn1RawDer; -use picky_krb::constants::gss_api::{ACCEPT_INCOMPLETE, TGT_REP_TOKEN_ID, TGT_REQ_TOKEN_ID}; -use picky_krb::constants::types::{NT_SRV_INST, TGT_REP_MSG_TYPE, TGT_REQ_MSG_TYPE}; -use picky_krb::data_types::{KerberosStringAsn1, PrincipalName, Ticket}; +use picky_krb::constants::gss_api::ACCEPT_INCOMPLETE; use picky_krb::gss_api::{ - ApplicationTag0, GssApiNegInit, KrbMessage, MechType, MechTypeList, NegTokenInit, NegTokenTarg, NegTokenTarg1, + ApplicationTag0, GssApiNegInit, MechType, MechTypeList, NegTokenInit, NegTokenTarg, NegTokenTarg1, }; -use picky_krb::messages::{TgtRep, TgtReq}; -use crate::{Error, ErrorKind, KERBEROS_VERSION, Result}; +use crate::{Error, ErrorKind, Result}; /// Generates supported mechanism type list. pub(super) fn generate_mech_type_list(kerberos: bool, ntlm: bool) -> Result { @@ -46,34 +42,10 @@ pub(super) fn generate_mech_type_list(kerberos: bool, ntlm: bool) -> Result, mech_list: MechTypeList, + mech_token: Option>, ) -> Result> { - let mech_token = if let Some(sname) = sname { - let sname = sname - .iter() - .map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string(sname.to_string())?))) - .collect::>>()?; - - let krb5_neg_token_init = ApplicationTag0(KrbMessage { - krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()), - krb5_token_id: TGT_REQ_TOKEN_ID, - krb_msg: TgtReq { - pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), - msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REQ_MSG_TYPE])), - server_name: ExplicitContextTag2::from(PrincipalName { - name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), - name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(sname)), - }), - }, - }); - - Some(ExplicitContextTag2::from(OctetStringAsn1::from( - picky_asn1_der::to_vec(&krb5_neg_token_init)?, - ))) - } else { - None - }; + let mech_token = mech_token.map(|token| ExplicitContextTag2::from(OctetStringAsn1::from(token))); Ok(ApplicationTag0(GssApiNegInit { oid: ObjectIdentifierAsn1::from(oids::spnego()), @@ -112,30 +84,18 @@ pub(super) fn generate_final_neg_token_targ( }) } -pub(super) fn generate_neg_token_targ(mech_type: ObjectIdentifier, tgt_rep: Option) -> Result { - let response_token = tgt_rep - .map(|tgt_rep| { - Result::Ok(ExplicitContextTag2::from(OctetStringAsn1::from( - picky_asn1_der::to_vec(&ApplicationTag0(KrbMessage { - krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()), - krb5_token_id: TGT_REP_TOKEN_ID, - krb_msg: tgt_rep, - }))?, - ))) - }) - .transpose()?; +pub(super) fn generate_neg_token_targ( + neg_result: Vec, + mech_type: ObjectIdentifier, + response_token: Option>, +) -> Result { + 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(ACCEPT_INCOMPLETE.to_vec())))), + 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), })) } - -pub(super) fn generate_tgt_rep(ticket: Ticket) -> TgtRep { - TgtRep { - pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), - msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REP_MSG_TYPE])), - ticket: ExplicitContextTag2::from(ticket), - } -} diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 36c3e7b9..30bf8593 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -5,6 +5,7 @@ mod generators; pub(crate) mod server; use std::fmt::Debug; +use std::mem; use std::net::IpAddr; use std::sync::LazyLock; @@ -12,6 +13,7 @@ pub use config::{NegotiateConfig, ProtocolConfig}; use picky::oids; use picky_krb::gss_api::MechType; +use crate::builders::{EmptyAcceptSecurityContext, FilledAcceptSecurityContext, FilledInitializeSecurityContext}; use crate::generator::{ GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal, }; @@ -21,10 +23,11 @@ use crate::ntlm::NtlmConfig; #[allow(unused)] use crate::utils::is_azure_ad_domain; use crate::{ - AcquireCredentialsHandleResult, AuthIdentity, CertTrustStatus, ContextNames, ContextSizes, CredentialUse, - Credentials, CredentialsBuffers, DecryptionFlags, Error, ErrorKind, Kerberos, KerberosConfig, Ntlm, - PACKAGE_ID_NONE, PackageCapabilities, PackageInfo, Pku2u, Result, SecurityBuffer, SecurityBufferRef, - SecurityPackageType, SecurityStatus, Sspi, SspiEx, SspiImpl, builders, kerberos, ntlm, pku2u, + 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"; @@ -73,9 +76,100 @@ impl NegotiatedProtocol { 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<'_, '_, ::CredentialsHandle>, + ) -> Result { + 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<'_, ::CredentialsHandle>, + ) -> Result { + 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> = + 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) + } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] enum NegotiateState { #[default] Initial, @@ -108,6 +202,12 @@ pub struct Negotiate { /// Encoded [MechTypeList]. Used for `mechListMIC` token verification. mech_types: Vec, mic_verified: bool, + /// Indicates whether `mechLitsMIC` 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)] @@ -173,6 +273,7 @@ impl Negotiate { mode, mech_types: Default::default(), mic_verified: false, + mic_needed: true, }) } @@ -225,6 +326,7 @@ impl Negotiate { 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) -> Result<()> { let enabled_packages = self.package_list; @@ -236,12 +338,27 @@ impl Negotiate { )); } + // 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(()); @@ -255,11 +372,24 @@ impl Negotiate { )); } + // 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()))); } + // [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(()); } @@ -399,6 +529,22 @@ impl Negotiate { } } + /// 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 @@ -415,16 +561,16 @@ impl<'a> Negotiate { pub(crate) async fn accept_security_context_impl( &'a mut self, yield_point: &mut YieldPointLocal, - builder: crate::FilledAcceptSecurityContext<'a, ::CredentialsHandle>, - ) -> Result { + builder: FilledAcceptSecurityContext<'a, ::CredentialsHandle>, + ) -> Result { 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 crate::FilledInitializeSecurityContext<'_, '_, ::CredentialsHandle>, - ) -> Result { + builder: &'a mut FilledInitializeSecurityContext<'_, '_, ::CredentialsHandle>, + ) -> Result { client::initialize_security_context(self, yield_point, builder).await } } @@ -664,7 +810,7 @@ impl SspiImpl for Negotiate { #[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)] fn accept_security_context_impl<'a>( &'a mut self, - builder: builders::FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, + builder: FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, ) -> Result> { Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move { server::accept_security_context(self, &mut yield_point, builder).await @@ -673,7 +819,7 @@ impl SspiImpl for Negotiate { fn initialize_security_context_impl<'ctx, 'b, 'g>( &'ctx mut self, - builder: &'b mut builders::FilledInitializeSecurityContext<'ctx, 'ctx, Self::CredentialsHandle>, + builder: &'b mut FilledInitializeSecurityContext<'ctx, 'ctx, Self::CredentialsHandle>, ) -> Result> where 'ctx: 'g, diff --git a/src/negotiate/server.rs b/src/negotiate/server.rs index f2673f3c..5fdf4ff8 100644 --- a/src/negotiate/server.rs +++ b/src/negotiate/server.rs @@ -5,15 +5,12 @@ use picky_krb::gss_api::{NegTokenTarg, NegTokenTarg1}; use crate::builders::FilledAcceptSecurityContext; use crate::generator::YieldPointLocal; -use crate::kerberos::server::as_exchange::request_tgt; use crate::negotiate::NegotiateState; 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, generate_tgt_rep, -}; +use crate::negotiate::generators::{generate_final_neg_token_targ, generate_neg_token_targ, generate_neg_token_targ_1}; use crate::{ - AcceptSecurityContextResult, BufferType, EmptyAcceptSecurityContext, Error, ErrorKind, Negotiate, - NegotiatedProtocol, Result, SecurityBuffer, SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, + AcceptSecurityContextResult, BufferType, Error, ErrorKind, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, + SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, }; /// Performs one authentication step. @@ -30,55 +27,103 @@ pub(crate) async fn accept_security_context( .as_mut() .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?; - let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; + let input_token = SecurityBuffer::find_buffer_mut(input, BufferType::Token)?; let status = match negotiate.state { NegotiateState::Initial => { - let (tgt_req, mech_types) = decode_initial_neg_init(&input_token.buffer)?; - let mech_type = negotiate_mech_type(&mech_types, negotiate.package_list, &mut negotiate.protocol)?; + 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 tgt_rep = if let (Some(tgt_req), NegotiatedProtocol::Kerberos(kerberos)) = - (tgt_req, &mut negotiate.protocol) - { - // If user sent us TgtReq than they want Kerberos User-to-User auth. - // At this point, we need to request TGT token in KDC and send it back to the user. - - if !builder - .context_requirements - .contains(ServerRequestFlags::USE_SESSION_KEY) - { - warn!( - "KRB5 U2U has been negotiated (requested by the client) but the USE_SESSION_KEY flag is not set." - ); - } + let mut status = SecurityStatus::ContinueNeeded; - kerberos.krb5_user_to_user = true; + 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; - let credentials = kerberos - .server - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "Kerberos server configuration not present"))? - .user - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "KRB5 U2U has been negotiated (requested by the client) but the user credentials are not preset in Kerberos server configuration"))? - .clone(); + negotiate.state = NegotiateState::InProgress; - Some(generate_tgt_rep( - request_tgt(kerberos, &credentials, &tgt_req, yield_point).await?, - )) + // 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 { - None + // 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 mut encoded_neg_token_targ = picky_asn1_der::to_vec(&generate_neg_token_targ(mech_type, tgt_rep)?)?; + 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 = mem::take(&mut encoded_neg_token_targ); + output_token.buffer = encoded_neg_token_targ; - negotiate.state = NegotiateState::InProgress; - - SecurityStatus::ContinueNeeded + status } NegotiateState::InProgress => { let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?; @@ -97,63 +142,33 @@ pub(crate) async fn accept_security_context( input_token.buffer.clear(); } - let mut output_tokens = builder.output.to_vec(); - let mut input_tokens = input.to_vec(); - - let mut creds_handle = builder.credentials_handle.as_ref().and_then(|creds| (*creds).clone()); - let mut result = match &mut negotiate.protocol { - NegotiatedProtocol::Pku2u(pku2u) => { - let mut creds_handle = creds_handle.and_then(|creds_handle| creds_handle.into_auth_identity()); - let new_builder: FilledAcceptSecurityContext<'_, Option> = - 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 = output_token.buffer.clone(); + let mut result = negotiate + .protocol + .accept_security_context(yield_point, &mut builder) + .await?; if result.status == SecurityStatus::Ok || result.status == SecurityStatus::CompleteNeeded { - negotiate.state = NegotiateState::VerifyMic; - result.status = SecurityStatus::ContinueNeeded; - let mech_list_mic = mech_list_mic.0.map(|token| token.0.0); - let neg_result = if mech_list_mic.is_some() { + 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; + } + + 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_final_neg_token(neg_result, negotiate, &mut builder)?; + prepare_neg_token(neg_result, negotiate, &mut builder)?; } else { // Wrap in a NegToken. let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; @@ -167,7 +182,7 @@ pub(crate) async fn accept_security_context( result.status } NegotiateState::VerifyMic => { - if !negotiate.mic_verified { + if !negotiate.mic_verified && negotiate.mic_needed { let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?; let NegTokenTarg { neg_result: _, @@ -190,10 +205,10 @@ pub(crate) async fn accept_security_context( SecurityStatus::Ok } - _ => { + NegotiateState::Ok => { return Err(Error::new( ErrorKind::OutOfSequence, - "initialize_security_context called after negotiation completed", + "accept_security_context called after negotiation completed", )); } }; @@ -205,7 +220,7 @@ pub(crate) async fn accept_security_context( }) } -fn prepare_final_neg_token( +fn prepare_neg_token( neg_result: Vec, negotiate: &mut Negotiate, builder: &mut FilledAcceptSecurityContext<'_, ::CredentialsHandle>, @@ -218,15 +233,17 @@ fn prepare_final_neg_token( None }; - let neg_token_targ = generate_final_neg_token_targ( - neg_result, - response_token, + 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)?; diff --git a/src/utils.rs b/src/utils.rs index 88b920e0..0fdf89b4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,6 +12,7 @@ use crate::{BufferType, Error, ErrorKind, Result, Secret, SecurityBufferFlags, S /// This is a temporary workaround until [`std::error::Report`] is stabilised /// (tracking issue: ), at which /// point callers can be migrated to `format!("{:#}", std::error::Report::new(e))`. +#[cfg(feature = "network_client")] pub(crate) fn write_error_chain(w: &mut impl std::fmt::Write, e: &dyn std::error::Error) -> std::fmt::Result { write!(w, "{e}")?; let mut source = e.source(); @@ -217,7 +218,7 @@ pub(crate) fn map_keb_error_code_to_sspi_error(krb_error_code: u32) -> (ErrorKin ErrorKind::NoTgtReply, "no TGT available to validate USER-TO-USER".into(), ), - KDC_ERR_WRONG_REALM => (ErrorKind::InvalidParameter, "wrong Realm".into()), + KDC_ERR_WRONG_REALM => (ErrorKind::NoAuthenticatingAuthority, "wrong Realm".into()), KRB_AP_ERR_USER_TO_USER_REQUIRED => (ErrorKind::KdcInvalidRequest, "ticket must be for USER-TO-USER".into()), KDC_ERR_CANT_VERIFY_CERTIFICATE => ( ErrorKind::KdcInvalidRequest, diff --git a/tests/sspi/client_server/kerberos/context_validator.rs b/tests/sspi/client_server/kerberos/context_validator.rs new file mode 100644 index 00000000..34dbed4b --- /dev/null +++ b/tests/sspi/client_server/kerberos/context_validator.rs @@ -0,0 +1,70 @@ +use sspi::NegotiatedProtocol; +use sspi::credssp::SspiContext; + +/// Helper-trait to implement SSPI context validation in tests. +/// +/// _Note_: this trait is not complete and may be extended in the future when needed. +pub(super) trait SspiContextValidator { + /// Validates the client SSPI context after the provided number of iterations. + fn validate_client(&mut self, step: usize, client: &SspiContext); +} + +/// Empty validator that does not perform any validation. +pub(super) struct EmptySspiContextValidator; + +impl SspiContextValidator for EmptySspiContextValidator { + fn validate_client(&mut self, _step: usize, _client: &SspiContext) {} +} + +/// Performs additional SPNEGO context validation for Kerberos over SPNEGO tests. +pub(super) struct SpnegoKerberosContextValidator; + +impl SspiContextValidator for SpnegoKerberosContextValidator { + fn validate_client(&mut self, _step: usize, client: &SspiContext) { + let SspiContext::Negotiate(negotiate) = client else { + panic!("Expected Negotiate context"); + }; + + assert!(matches!( + negotiate.negotiated_protocol(), + NegotiatedProtocol::Kerberos(_) + )); + } +} + +/// Validates that the client correctly falls back to NTLM. +pub(super) struct SpnegoKerberosNtlmFallbackValidator; + +impl SspiContextValidator for SpnegoKerberosNtlmFallbackValidator { + fn validate_client(&mut self, _step: usize, client: &SspiContext) { + let SspiContext::Negotiate(negotiate) = client else { + panic!("Expected Negotiate context"); + }; + + assert!(matches!(negotiate.negotiated_protocol(), NegotiatedProtocol::Ntlm(_))); + } +} + +/// Validates that the client correctly falls back to NTLM when the server selected NTLM in SPNEGO instead of Kerberos. +pub(super) struct SpnegoServerNtlmFallbackValidator; + +impl SspiContextValidator for SpnegoServerNtlmFallbackValidator { + fn validate_client(&mut self, step: usize, client: &SspiContext) { + let SspiContext::Negotiate(negotiate) = client else { + panic!("Expected Negotiate context"); + }; + + match step { + 0 => { + assert!(matches!( + negotiate.negotiated_protocol(), + NegotiatedProtocol::Kerberos(_) + )); + } + 1 => { + assert!(matches!(negotiate.negotiated_protocol(), NegotiatedProtocol::Ntlm(_))); + } + _ => {} + } + } +} diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs index 113733c6..c3750655 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -1,5 +1,6 @@ #![allow(clippy::result_large_err)] +mod context_validator; pub(super) mod kdc; pub(super) mod network_client; @@ -16,15 +17,19 @@ use sspi::kerberos::ServerProperties; use sspi::network_client::NetworkClient; use sspi::{ AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, Kerberos, - KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, SecurityBuffer, SecurityStatus, - ServerRequestFlags, Sspi, SspiImpl, Username, + KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, NegotiatedProtocol, SecurityBuffer, + SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, }; use url::Url; +use crate::client_server::kerberos::context_validator::{ + EmptySspiContextValidator, SpnegoKerberosContextValidator, SpnegoKerberosNtlmFallbackValidator, + SpnegoServerNtlmFallbackValidator, SspiContextValidator, +}; use crate::client_server::kerberos::kdc::{ CLIENT_COMPUTER_NAME, KDC_URL, KdcMock, MAX_TIME_SKEW, PasswordCreds, SERVER_COMPUTER_NAME, UserName, Validators, }; -use crate::client_server::kerberos::network_client::NetworkClientMock; +use crate::client_server::kerberos::network_client::{FailedNetworkClientMock, NetworkClientMock}; use crate::client_server::{test_encryption, test_rpc_request_encryption, test_stream_buffer_encryption}; /// Represents a Kerberos environment: @@ -208,10 +213,12 @@ fn run_kerberos( network_client: &mut dyn NetworkClient, steps: usize, + mut context_validator: impl SspiContextValidator, ) { let mut client_in_token = Vec::new(); - for _ in 0..steps { + for step in 0..steps { + println!("----------------------"); let (client_status, token) = initialize_security_context( client, client_credentials_handle, @@ -221,7 +228,16 @@ fn run_kerberos( network_client, ); + println!("client status: {client_status:?}, token: {token:?}"); + + context_validator.validate_client(step, client); + if client_status == SecurityStatus::Ok { + println!("token when client is finished: {token:?}"); + if !token.is_empty() { + accept_security_context(server, server_credentials_handle, server_flags, token, network_client); + } + test_encryption(client, server); test_stream_buffer_encryption(client, server); test_rpc_request_encryption(client, server); @@ -231,6 +247,8 @@ fn run_kerberos( let (_, token) = accept_security_context(server, server_credentials_handle, server_flags, token, network_client); client_in_token = token; + + println!("client {client_in_token:?}") } panic!("Kerberos authentication should not exceed {steps} steps"); @@ -327,6 +345,7 @@ fn kerberos_auth() { server_flags, &mut network_client, 2, + EmptySspiContextValidator, ); } @@ -355,21 +374,12 @@ fn spnego_kerberos_u2u() { as_req: Box::new(|_as_req| { // Nothing to validate in AsReq. }), - tgs_req: Box::new(|tgs_req| { - // Here, we should check that the Kerberos client successfully negotiated Kerberos U2U auth. - - let kdc_options = tgs_req.0.req_body.kdc_options.0.0.as_bytes(); - // KDC options must have enc-tkt-in-skey enabled. - assert_eq!(kdc_options[4], 0x08, "the enc-tkt-in-skey KDC option is not enabled"); - - if let Some(tickets) = tgs_req.0.req_body.0.additional_tickets.0.as_ref() { - assert!( - !tickets.0.0.is_empty(), - "TgsReq must have at least one additional ticket: TGT from the application service" - ); - } else { - panic!("TgsReq must have at least one additional ticket: TGT from the application service"); - } + tgs_req: Box::new(|_tgs_req| { + // Nothing to validate in TgsReq. + // + // Previously, we were able to validate the presence of the additional ticket and enc-tkt-in-skey flag. + // But since we use a preflight Kerberos exchange to check if the Kerberos is possible, + // we can no longer do that. }), }, ); @@ -441,10 +451,19 @@ fn spnego_kerberos_u2u() { server_flags, &mut network_client, 3, + SpnegoKerberosContextValidator, ); } -fn run_spnego_kerberos(client_flags: ClientRequestFlags, server_flags: ServerRequestFlags, steps: usize) { +fn run_spnego( + client_flags: ClientRequestFlags, + server_flags: ServerRequestFlags, + steps: usize, + get_network_client: impl Fn(KdcMock) -> Box, + client_package_list: Option, + server_package_list: Option, + context_validator: impl SspiContextValidator, +) -> (SspiContext, SspiContext) { let KrbEnvironment { realm, credentials, @@ -473,18 +492,20 @@ fn run_spnego_kerberos(client_flags: ClientRequestFlags, server_flags: ServerReq }), }, ); - let mut network_client = NetworkClientMock { kdc }; + let mut network_client = get_network_client(kdc); let client_config = KerberosConfig { kdc_url: Some(Url::parse(KDC_URL).unwrap()), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; - let spnego_client = Negotiate::new_client(NegotiateConfig::new( - Box::new(client_config.clone()), - Some(String::from("kerberos,!ntlm")), - CLIENT_COMPUTER_NAME.into(), - )) - .unwrap(); + let mut spnego_client = SspiContext::Negotiate( + Negotiate::new_client(NegotiateConfig::new( + Box::new(client_config.clone()), + client_package_list.clone(), + CLIENT_COMPUTER_NAME.into(), + )) + .unwrap(), + ); let server_config = KerberosConfig { kdc_url: Some(Url::parse(KDC_URL).unwrap()), @@ -503,35 +524,40 @@ fn run_spnego_kerberos(client_flags: ClientRequestFlags, server_flags: ServerReq kerberos_config: server_config, server_properties, }; - let spnego_server = Negotiate::new_server( - NegotiateConfig::new( - Box::new(kerberos_server_config), - Some(String::from("kerberos,!ntlm")), - SERVER_COMPUTER_NAME.into(), - ), - vec![identity_1, identity_2], - ) - .unwrap(); + let mut spnego_server = SspiContext::Negotiate( + Negotiate::new_server( + NegotiateConfig::new( + Box::new(kerberos_server_config), + server_package_list.clone(), + SERVER_COMPUTER_NAME.into(), + ), + vec![identity_1, identity_2], + ) + .unwrap(), + ); let credentials = CredentialsBuffers::try_from(credentials).unwrap(); let mut client_credentials_handle = Some(credentials.clone()); let mut server_credentials_handle = Some(credentials); run_kerberos( - &mut SspiContext::Negotiate(spnego_client), + &mut spnego_client, &mut client_credentials_handle, client_flags, &target_name, - &mut SspiContext::Negotiate(spnego_server), + &mut spnego_server, &mut server_credentials_handle, server_flags, - &mut network_client, + &mut *network_client, steps, + context_validator, ); + + (spnego_client, spnego_server) } #[test] -fn spnego_kerberos() { +fn spnego_kerberos_1() { let client_flags = ClientRequestFlags::MUTUAL_AUTH | ClientRequestFlags::INTEGRITY | ClientRequestFlags::SEQUENCE_DETECT @@ -542,8 +568,24 @@ fn spnego_kerberos() { | ServerRequestFlags::SEQUENCE_DETECT | ServerRequestFlags::REPLAY_DETECT | ServerRequestFlags::CONFIDENTIALITY; + let package_list = Some(String::from("kerberos,ntlm")); - run_spnego_kerberos(client_flags, server_flags, 3); + let (client, _server) = run_spnego( + client_flags, + server_flags, + 3, + |kdc| Box::new(NetworkClientMock { kdc }), + package_list.clone(), + package_list, + SpnegoKerberosContextValidator, + ); + + let SspiContext::Negotiate(negotiate) = client else { + panic!("client must be a Negotiate context"); + }; + let negotiated_protocol = negotiate.negotiated_protocol(); + + assert!(matches!(negotiated_protocol, NegotiatedProtocol::Kerberos(_)),); } #[test] @@ -560,6 +602,92 @@ fn spnego_kerberos_dce_style() { | ServerRequestFlags::SEQUENCE_DETECT | ServerRequestFlags::REPLAY_DETECT | ServerRequestFlags::CONFIDENTIALITY; + let package_list = Some(String::from("kerberos,ntlm")); + + let (client, _server) = run_spnego( + client_flags, + server_flags, + 4, + |kdc| Box::new(NetworkClientMock { kdc }), + package_list.clone(), + package_list, + SpnegoKerberosContextValidator, + ); + + let SspiContext::Negotiate(negotiate) = client else { + panic!("client must be a Negotiate context"); + }; + let negotiated_protocol = negotiate.negotiated_protocol(); + + assert!(matches!(negotiated_protocol, NegotiatedProtocol::Kerberos(_)),); +} + +#[test] +fn spnego_kerberos_ntlm_fallback() { + let client_flags = ClientRequestFlags::MUTUAL_AUTH + | ClientRequestFlags::INTEGRITY + | ClientRequestFlags::SEQUENCE_DETECT + | ClientRequestFlags::REPLAY_DETECT + | ClientRequestFlags::CONFIDENTIALITY; + let server_flags = ServerRequestFlags::MUTUAL_AUTH + | ServerRequestFlags::INTEGRITY + | ServerRequestFlags::SEQUENCE_DETECT + | ServerRequestFlags::REPLAY_DETECT + | ServerRequestFlags::CONFIDENTIALITY; + let package_list = Some(String::from("kerberos,ntlm")); + + for kind in sspi::FALLBACK_ERROR_KINDS { + let (client, _server) = run_spnego( + client_flags, + server_flags, + 4, + |_| Box::new(FailedNetworkClientMock { kind }), + package_list.clone(), + package_list.clone(), + SpnegoKerberosNtlmFallbackValidator, + ); + + let SspiContext::Negotiate(negotiate) = client else { + panic!("client must be a Negotiate context"); + }; + let negotiated_protocol = negotiate.negotiated_protocol(); + + assert!( + matches!(negotiated_protocol, NegotiatedProtocol::Ntlm(_)), + "Client should fallback to NTLM if Kerberos fails with {kind:?} error" + ); + } +} + +#[test] +fn spnego_kerberos_server_ntlm_fallback() { + let client_flags = ClientRequestFlags::MUTUAL_AUTH + | ClientRequestFlags::INTEGRITY + | ClientRequestFlags::SEQUENCE_DETECT + | ClientRequestFlags::REPLAY_DETECT + | ClientRequestFlags::CONFIDENTIALITY; + let server_flags = ServerRequestFlags::MUTUAL_AUTH + | ServerRequestFlags::INTEGRITY + | ServerRequestFlags::SEQUENCE_DETECT + | ServerRequestFlags::REPLAY_DETECT + | ServerRequestFlags::CONFIDENTIALITY; + let client_package_list = Some(String::from("kerberos,ntlm")); + let server_package_list = Some(String::from("!kerberos,ntlm")); + + let (client, _server) = run_spnego( + client_flags, + server_flags, + 4, + |kdc| Box::new(NetworkClientMock { kdc }), + client_package_list, + server_package_list, + SpnegoServerNtlmFallbackValidator, + ); + + let SspiContext::Negotiate(negotiate) = client else { + panic!("client must be a Negotiate context"); + }; + let negotiated_protocol = negotiate.negotiated_protocol(); - run_spnego_kerberos(client_flags, server_flags, 4); + assert!(matches!(negotiated_protocol, NegotiatedProtocol::Ntlm(_)),); } diff --git a/tests/sspi/client_server/kerberos/network_client.rs b/tests/sspi/client_server/kerberos/network_client.rs index 0fa53759..6617cdc2 100644 --- a/tests/sspi/client_server/kerberos/network_client.rs +++ b/tests/sspi/client_server/kerberos/network_client.rs @@ -1,6 +1,6 @@ -use sspi::Result; use sspi::generator::NetworkRequest; use sspi::network_client::NetworkClient; +use sspi::{ErrorKind, Result}; use crate::client_server::kerberos::kdc::KdcMock; @@ -37,3 +37,17 @@ impl NetworkClient for NetworkClientMock { Ok(data) } } + +/// [NetworkClient] that returns an error for every request. +/// +/// The purpose of this specific mock is to test Kerberos to NTLM fallback in Negotiate (SPNEGO). +pub(crate) struct FailedNetworkClientMock { + /// Error kind to return for every request. + pub kind: ErrorKind, +} + +impl NetworkClient for FailedNetworkClientMock { + fn send(&self, _request: &NetworkRequest) -> Result> { + Err(sspi::Error::new(self.kind, "error from mock network client")) + } +} diff --git a/tests/sspi/client_server/mod.rs b/tests/sspi/client_server/mod.rs index 1ac75bb7..8a131a6a 100644 --- a/tests/sspi/client_server/mod.rs +++ b/tests/sspi/client_server/mod.rs @@ -1,4 +1,5 @@ -#![cfg(feature = "network_client")] // The network_client feature is required for the client_server tests. +// The network_client and __test-data features are required for the client_server tests. +#![cfg(all(feature = "network_client", feature = "__test-data"))] mod credssp; mod kerberos; diff --git a/tests/sspi/client_server/negotiate.rs b/tests/sspi/client_server/negotiate.rs index 516f4bc5..17a97f14 100644 --- a/tests/sspi/client_server/negotiate.rs +++ b/tests/sspi/client_server/negotiate.rs @@ -83,16 +83,18 @@ fn run_spnego_ntlm() { input_token[0].buffer.clear(); - let builder = server - .accept_security_context() - .with_credentials_handle(&mut server_credentials_handle) - .with_context_requirements(ServerRequestFlags::empty()) - .with_target_data_representation(DataRepresentation::Native) - .with_input(&mut output_token) - .with_output(&mut input_token); - server.accept_security_context_sync(builder).unwrap(); + if !output_token[0].buffer.is_empty() { + let builder = server + .accept_security_context() + .with_credentials_handle(&mut server_credentials_handle) + .with_context_requirements(ServerRequestFlags::empty()) + .with_target_data_representation(DataRepresentation::Native) + .with_input(&mut output_token) + .with_output(&mut input_token); + server.accept_security_context_sync(builder).unwrap(); - output_token[0].buffer.clear(); + output_token[0].buffer.clear(); + } if status == SecurityStatus::Ok { test_encryption(&mut client, &mut server); diff --git a/tools/dpapi-cli-client/src/logging.rs b/tools/dpapi-cli-client/src/logging.rs index 5ad55c5d..fde27e1a 100644 --- a/tools/dpapi-cli-client/src/logging.rs +++ b/tools/dpapi-cli-client/src/logging.rs @@ -1,7 +1,7 @@ use std::fs::OpenOptions; -use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; +use tracing_subscriber::prelude::*; const DPAPI_LOG_PATH_ENV: &str = "DPAPI_LOG_PATH"; diff --git a/tools/dpapi-cli-client/src/main.rs b/tools/dpapi-cli-client/src/main.rs index 967a5636..0db26bec 100644 --- a/tools/dpapi-cli-client/src/main.rs +++ b/tools/dpapi-cli-client/src/main.rs @@ -8,7 +8,7 @@ mod network_client; mod session_token; use std::fs; -use std::io::{stdin, stdout, Error, ErrorKind, Read, Result, Write}; +use std::io::{Error, ErrorKind, Read, Result, Write, stdin, stdout}; use dpapi::{CryptProtectSecretArgs, CryptUnprotectSecretArgs}; use dpapi_native_transport::NativeTransport;