From 6b910896796cd21ed7d09aa1a59f02cc73183bf2 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Wed, 11 Mar 2026 16:47:01 +0200 Subject: [PATCH 01/24] feat(sspi): negotiate: improve ntlm fallback; --- src/negotiate/client.rs | 144 ++++++++++++++++++++++++++++++++++------ src/negotiate/mod.rs | 18 +++++ 2 files changed, 141 insertions(+), 21 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index eedf5215..1d28e3c2 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -11,7 +11,8 @@ use crate::negotiate::generators::{ use crate::utils::parse_target_name; use crate::{ AuthIdentity, BufferType, ClientRequestFlags, ClientResponseFlags, CredentialsBuffers, Error, ErrorKind, - InitializeSecurityContextResult, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, SecurityStatus, SspiImpl, + InitializeSecurityContextResult, Kerberos, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, SecurityStatus, + SspiImpl, }; /// Performs one authentication step. @@ -67,10 +68,11 @@ pub(crate) async fn initialize_security_context<'a>( match negotiate.state { NegotiateState::Initial => { - let sname = if builder + let is_u2u = builder .context_requirements - .contains(ClientRequestFlags::USE_SESSION_KEY) - { + .contains(ClientRequestFlags::USE_SESSION_KEY); + + let sname = if is_u2u { let (service_name, service_principal_name) = parse_target_name(builder.target_name.ok_or_else(|| { Error::new( @@ -86,6 +88,59 @@ pub(crate) async fn initialize_security_context<'a>( debug!(?sname); + // Try optimistic Kerberos authentication if the negotiated protocol is Kerberos. + let first_krb_token = if let NegotiatedProtocol::Kerberos(kerberos) = &mut negotiate.protocol { + // We try to call `initialize_security_context` on the Kerberos security package. + // If this call does not succeed we fallback to NTLM. But if this call succeeds, we can save the output Kerberos token + // and reuse it on the second Negotiate `initialize_security_context` call. Also, we need to take Kerberos U2U into the account. + // If the Kerberos U2U is used, then we cannot reuse the output Kerberos token, because it does not contain a TGT ticket + // and `enc-tkt-in-skey` is not enabled. + // + // So, we clone the Kerberos context if U2U is used. This way, the Kerberos state will not be in progress on the second `initialize_security_context` call, + // and we can processed normally. But if the U2U is not used, then we will reuse the token and the Kerberos state. + let kerberos = if is_u2u { &mut kerberos.clone() } else { kerberos }; + match try_kerberos_optimistic(kerberos, yield_point, builder).await { + Ok(token) => { + debug!("Optimistic Kerberos authentication succeeded"); + + // We cannot reuse the token if the U2U is used, because it does not contain a TGT ticket and `enc-tkt-in-skey` is not enabled. + if is_u2u { None } else { Some(token) } + } + // 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. + // - ErrorKind::CertificateUnknown: The KDC proxy certificate is invalid. + Err(err) + if [ + ErrorKind::TimeSkew, + ErrorKind::NoAuthenticatingAuthority, + ErrorKind::CertificateUnknown, + ] + .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."); + + return Err(err); + } + + debug!("Fallback to NTLM succeeded"); + + None + } + Err(err) => { + return Err(err); + } + } + } else { + None + }; + negotiate.first_kdc_token = first_krb_token; + let mech_types = generate_mech_type_list( matches!(&negotiate.protocol, NegotiatedProtocol::Kerberos(_)), negotiate.package_list.ntlm, @@ -147,29 +202,44 @@ 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)?; + let mut result = if let Some(token) = negotiate.first_kdc_token.take() { + debug!("Using Kerberos token from the first optimistic Kerberos call."); - builder.output = mem::take(&mut transformed_builder.output); + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer = token; - result + InitializeSecurityContextResult { + status: SecurityStatus::ContinueNeeded, + flags: ClientResponseFlags::empty(), + expiry: None, } - 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)); + } else { + 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)?; - let result = ntlm.initialize_security_context_impl(&mut transformed_builder)?; + builder.output = mem::take(&mut transformed_builder.output); - 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)); - result + let result = ntlm.initialize_security_context_impl(&mut transformed_builder)?; + + builder.output = mem::take(&mut transformed_builder.output); + + result + } } }; @@ -276,3 +346,35 @@ fn prepare_final_neg_token( Ok(()) } + +/// Attempts optimistic Kerberos authentication. +/// +/// This function calls Kerberos's `initialize_security_context_impl` to generate an initial +/// Kerberos authentication (which performs AS and TGS exchanges). +/// +/// Returns the Kerberos token on success, or an error if the authentication fails. +/// The caller should handle the error kind and specially to fallback to NTLM in some cases (like `ErrorKind::TimeSkew`). +#[instrument(ret, skip_all)] +async fn try_kerberos_optimistic<'a>( + kerberos: &'a mut Kerberos, + yield_point: &mut YieldPointLocal, + builder: &'a mut crate::builders::FilledInitializeSecurityContext< + '_, + '_, + ::CredentialsHandle, + >, +) -> Result> { + let result = kerberos.initialize_security_context_impl(yield_point, builder).await?; + + // Check that the call succeeded (or is in progress) + if result.status != SecurityStatus::ContinueNeeded { + return Err(Error::new( + ErrorKind::InternalError, + format!("unexpected Kerberos status: {:?}", result.status), + )); + } + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + + Ok(mem::take(&mut output_token.buffer)) +} diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 36c3e7b9..60a57357 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -108,6 +108,7 @@ pub struct Negotiate { /// Encoded [MechTypeList]. Used for `mechListMIC` token verification. mech_types: Vec, mic_verified: bool, + first_kdc_token: Option>, } #[derive(Clone, Copy, Debug)] @@ -173,6 +174,7 @@ impl Negotiate { mode, mech_types: Default::default(), mic_verified: false, + first_kdc_token: None, }) } @@ -399,6 +401,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 From a80a5202177b45edfbba891aff54e5c95d3210c7 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 12 Mar 2026 14:50:24 +0200 Subject: [PATCH 02/24] test(sspi): spnego: kerberos to ntlm fallback; --- tests/sspi/client_server/kerberos/mod.rs | 83 +++++++++++++------ .../client_server/kerberos/network_client.rs | 16 +++- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs index 113733c6..92c6bd74 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -15,8 +15,8 @@ use sspi::credssp::SspiContext; use sspi::kerberos::ServerProperties; use sspi::network_client::NetworkClient; use sspi::{ - AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, Kerberos, - KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, SecurityBuffer, SecurityStatus, + AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, ErrorKind, + Kerberos, KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, SecurityBuffer, SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, }; use url::Url; @@ -24,7 +24,7 @@ use url::Url; 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: @@ -355,21 +355,8 @@ 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. }), }, ); @@ -444,7 +431,13 @@ fn spnego_kerberos_u2u() { ); } -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, + package_list: Option, +) { let KrbEnvironment { realm, credentials, @@ -473,7 +466,7 @@ 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()), @@ -481,7 +474,7 @@ fn run_spnego_kerberos(client_flags: ClientRequestFlags, server_flags: ServerReq }; let spnego_client = Negotiate::new_client(NegotiateConfig::new( Box::new(client_config.clone()), - Some(String::from("kerberos,!ntlm")), + package_list.clone(), CLIENT_COMPUTER_NAME.into(), )) .unwrap(); @@ -506,7 +499,7 @@ fn run_spnego_kerberos(client_flags: ClientRequestFlags, server_flags: ServerReq let spnego_server = Negotiate::new_server( NegotiateConfig::new( Box::new(kerberos_server_config), - Some(String::from("kerberos,!ntlm")), + package_list.clone(), SERVER_COMPUTER_NAME.into(), ), vec![identity_1, identity_2], @@ -525,7 +518,7 @@ fn run_spnego_kerberos(client_flags: ClientRequestFlags, server_flags: ServerReq &mut SspiContext::Negotiate(spnego_server), &mut server_credentials_handle, server_flags, - &mut network_client, + &mut *network_client, steps, ); } @@ -543,7 +536,13 @@ fn spnego_kerberos() { | ServerRequestFlags::REPLAY_DETECT | ServerRequestFlags::CONFIDENTIALITY; - run_spnego_kerberos(client_flags, server_flags, 3); + run_spnego( + client_flags, + server_flags, + 3, + |kdc| Box::new(NetworkClientMock { kdc }), + Some(String::from("kerberos,!ntlm")), + ); } #[test] @@ -561,5 +560,39 @@ fn spnego_kerberos_dce_style() { | ServerRequestFlags::REPLAY_DETECT | ServerRequestFlags::CONFIDENTIALITY; - run_spnego_kerberos(client_flags, server_flags, 4); + run_spnego( + client_flags, + server_flags, + 4, + |kdc| Box::new(NetworkClientMock { kdc }), + Some(String::from("kerberos,!ntlm")), + ); +} + +#[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; + + for kind in [ + ErrorKind::TimeSkew, + ErrorKind::NoAuthenticatingAuthority, + ErrorKind::CertificateUnknown, + ] { + run_spnego( + client_flags, + server_flags, + 4, + |_| Box::new(FailedNetworkClientMock { kind }), + Some(String::from("kerberos,ntlm")), + ); + } } diff --git a/tests/sspi/client_server/kerberos/network_client.rs b/tests/sspi/client_server/kerberos/network_client.rs index 0fa53759..0ab06d4a 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".to_owned())) + } +} From f7b1a0c669c7465801a7c67f9426c09b6f315244 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 12 Mar 2026 14:56:09 +0200 Subject: [PATCH 03/24] feat(sspi): negotiate: clarify some behavior in comments; --- src/negotiate/client.rs | 3 +-- src/negotiate/mod.rs | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 1d28e3c2..6f553472 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -123,7 +123,7 @@ pub(crate) async fn initialize_security_context<'a>( warn!("Kerberos authentication failed with {err} error, attempting NTLM fallback."); if !negotiate.fallback_to_ntlm() { - warn!("Failed to fallback to NTLM."); + warn!("Failed to fallback to NTLM: NTLM is disabled."); return Err(err); } @@ -366,7 +366,6 @@ async fn try_kerberos_optimistic<'a>( ) -> Result> { let result = kerberos.initialize_security_context_impl(yield_point, builder).await?; - // Check that the call succeeded (or is in progress) if result.status != SecurityStatus::ContinueNeeded { return Err(Error::new( ErrorKind::InternalError, diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 60a57357..7a00ccd2 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -108,6 +108,11 @@ pub struct Negotiate { /// Encoded [MechTypeList]. Used for `mechListMIC` token verification. mech_types: Vec, mic_verified: bool, + /// On the first `initialize_security_context` call we call the Kerberos `initialize_security_context` to get the initial token. + /// This is an advanced mechanism for protocol negotiation. For example, if KDC does not work properly or time skew is too big, + /// then we fallback to NTLM and continue the NLA. + /// But if the first token is generated successfully (i.g. Kerberos works properly), then we save this token and reuse on + /// the second `initialize_security_context` call. first_kdc_token: Option>, } From c7bcb714aaa82309607a00bfe02800a7d053640c Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 12 Mar 2026 15:09:23 +0200 Subject: [PATCH 04/24] refactor(tests): small refactoring; --- tests/sspi/client_server/kerberos/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs index 92c6bd74..434117b8 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -499,7 +499,7 @@ fn run_spnego( let spnego_server = Negotiate::new_server( NegotiateConfig::new( Box::new(kerberos_server_config), - package_list.clone(), + package_list, SERVER_COMPUTER_NAME.into(), ), vec![identity_1, identity_2], From d6334c0a092a745f55c21b933c4bca4d933bd59d Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 12 Mar 2026 17:40:02 +0200 Subject: [PATCH 05/24] refactor(tests): small refactoring; --- src/negotiate/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 7a00ccd2..08d24cc2 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -111,7 +111,7 @@ pub struct Negotiate { /// On the first `initialize_security_context` call we call the Kerberos `initialize_security_context` to get the initial token. /// This is an advanced mechanism for protocol negotiation. For example, if KDC does not work properly or time skew is too big, /// then we fallback to NTLM and continue the NLA. - /// But if the first token is generated successfully (i.g. Kerberos works properly), then we save this token and reuse on + /// But if the first token is generated successfully (i.e. Kerberos works properly), then we save this token and reuse on /// the second `initialize_security_context` call. first_kdc_token: Option>, } From c3e87a2044ecf5900c110739755ed1025d3d908a Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 12 Mar 2026 21:59:55 +0200 Subject: [PATCH 06/24] refactor: small refactoring; --- src/negotiate/client.rs | 6 +- src/negotiate/mod.rs | 4 +- tests/sspi/client_server/kerberos/mod.rs | 78 +++++++++++++------ .../client_server/kerberos/network_client.rs | 2 +- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 6f553472..a4ed20c8 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -97,7 +97,7 @@ pub(crate) async fn initialize_security_context<'a>( // and `enc-tkt-in-skey` is not enabled. // // So, we clone the Kerberos context if U2U is used. This way, the Kerberos state will not be in progress on the second `initialize_security_context` call, - // and we can processed normally. But if the U2U is not used, then we will reuse the token and the Kerberos state. + // and we can process normally. But if the U2U is not used, then we will reuse the token and the Kerberos state. let kerberos = if is_u2u { &mut kerberos.clone() } else { kerberos }; match try_kerberos_optimistic(kerberos, yield_point, builder).await { Ok(token) => { @@ -202,7 +202,9 @@ pub(crate) async fn initialize_security_context<'a>( input_token.buffer.clear(); } - let mut result = if let Some(token) = negotiate.first_kdc_token.take() { + let mut result = if matches!(negotiate.protocol, NegotiatedProtocol::Kerberos(_)) + && let Some(token) = negotiate.first_kdc_token.take() + { debug!("Using Kerberos token from the first optimistic Kerberos call."); let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 08d24cc2..ad68a46c 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -407,8 +407,8 @@ impl Negotiate { } /// Fallback to NTLM protocol. - // - // Returns true if the fallback was successful, false if NTLM is disabled and fallback is not possible. + /// + /// 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; diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs index 434117b8..339fa4f2 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -16,8 +16,8 @@ use sspi::kerberos::ServerProperties; use sspi::network_client::NetworkClient; use sspi::{ AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, ErrorKind, - Kerberos, KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, SecurityBuffer, SecurityStatus, - ServerRequestFlags, Sspi, SspiImpl, Username, + Kerberos, KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, NegotiatedProtocol, SecurityBuffer, + SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, }; use url::Url; @@ -437,7 +437,7 @@ fn run_spnego( steps: usize, get_network_client: impl Fn(KdcMock) -> Box, package_list: Option, -) { +) -> (SspiContext, SspiContext) { let KrbEnvironment { realm, credentials, @@ -472,12 +472,14 @@ fn run_spnego( 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()), - package_list.clone(), - CLIENT_COMPUTER_NAME.into(), - )) - .unwrap(); + let mut spnego_client = SspiContext::Negotiate( + Negotiate::new_client(NegotiateConfig::new( + Box::new(client_config.clone()), + package_list.clone(), + CLIENT_COMPUTER_NAME.into(), + )) + .unwrap(), + ); let server_config = KerberosConfig { kdc_url: Some(Url::parse(KDC_URL).unwrap()), @@ -496,31 +498,35 @@ fn run_spnego( kerberos_config: server_config, server_properties, }; - let spnego_server = Negotiate::new_server( - NegotiateConfig::new( - Box::new(kerberos_server_config), - package_list, - 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), + package_list, + 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, steps, ); + + (spnego_client, spnego_server) } #[test] @@ -536,13 +542,20 @@ fn spnego_kerberos() { | ServerRequestFlags::REPLAY_DETECT | ServerRequestFlags::CONFIDENTIALITY; - run_spnego( + let (client, _server) = run_spnego( client_flags, server_flags, 3, |kdc| Box::new(NetworkClientMock { kdc }), Some(String::from("kerberos,!ntlm")), ); + + 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,13 +573,20 @@ fn spnego_kerberos_dce_style() { | ServerRequestFlags::REPLAY_DETECT | ServerRequestFlags::CONFIDENTIALITY; - run_spnego( + let (client, _server) = run_spnego( client_flags, server_flags, 4, |kdc| Box::new(NetworkClientMock { kdc }), - Some(String::from("kerberos,!ntlm")), + Some(String::from("kerberos,ntlm")), ); + + 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] @@ -587,12 +607,22 @@ fn spnego_kerberos_ntlm_fallback() { ErrorKind::NoAuthenticatingAuthority, ErrorKind::CertificateUnknown, ] { - run_spnego( + let (client, _server) = run_spnego( client_flags, server_flags, 4, |_| Box::new(FailedNetworkClientMock { kind }), Some(String::from("kerberos,ntlm")), ); + + 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" + ); } } diff --git a/tests/sspi/client_server/kerberos/network_client.rs b/tests/sspi/client_server/kerberos/network_client.rs index 0ab06d4a..e0ced2f3 100644 --- a/tests/sspi/client_server/kerberos/network_client.rs +++ b/tests/sspi/client_server/kerberos/network_client.rs @@ -48,6 +48,6 @@ pub(crate) struct FailedNetworkClientMock { impl NetworkClient for FailedNetworkClientMock { fn send(&self, _request: &NetworkRequest) -> Result> { - Err(sspi::Error::new(self.kind, "Error from mock network client".to_owned())) + Err(sspi::Error::new(self.kind, "error from mock network client".to_owned())) } } From c8261822d5615c4f4973e41e87e67421c4e9c4c5 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 12 Mar 2026 22:04:24 +0200 Subject: [PATCH 07/24] refactor: small refactoring; --- src/negotiate/client.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index a4ed20c8..406c73ac 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -15,6 +15,18 @@ use crate::{ 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. +/// - ErrorKind::CertificateUnknown: The KDC proxy certificate is invalid. +const NTLM_FALLBACK_ERROR_KINDS: [ErrorKind; 3] = [ + ErrorKind::TimeSkew, + ErrorKind::NoAuthenticatingAuthority, + ErrorKind::CertificateUnknown, +]; + /// Performs one authentication step. /// /// The user should call this function until it returns `SecurityStatus::Ok`. @@ -106,20 +118,7 @@ pub(crate) async fn initialize_security_context<'a>( // We cannot reuse the token if the U2U is used, because it does not contain a TGT ticket and `enc-tkt-in-skey` is not enabled. if is_u2u { None } else { Some(token) } } - // 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. - // - ErrorKind::CertificateUnknown: The KDC proxy certificate is invalid. - Err(err) - if [ - ErrorKind::TimeSkew, - ErrorKind::NoAuthenticatingAuthority, - ErrorKind::CertificateUnknown, - ] - .contains(&err.error_type) => - { + Err(err) if NTLM_FALLBACK_ERROR_KINDS.contains(&err.error_type) => { warn!("Kerberos authentication failed with {err} error, attempting NTLM fallback."); if !negotiate.fallback_to_ntlm() { From 8f1fa77ddb5c2b7762f0bf9e8ef79ad96391aa81 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Fri, 13 Mar 2026 18:37:41 +0200 Subject: [PATCH 08/24] refactor: small refactoring; --- src/negotiate/client.rs | 7 ++- src/negotiate/mod.rs | 9 +++- tests/sspi/client_server/kerberos/mod.rs | 55 +++++++++++++++++++++--- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 406c73ac..89dc9d01 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -188,7 +188,8 @@ pub(crate) async fn initialize_security_context<'a>( 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)?; } @@ -355,6 +356,10 @@ fn prepare_final_neg_token( /// /// Returns the Kerberos token on success, or an error if the authentication fails. /// The caller should handle the error kind and specially to fallback to NTLM in some cases (like `ErrorKind::TimeSkew`). +/// +/// The function will empty the output [BufferType::Token] buffer in the builder +/// and return the Kerberos token as a plain `Vec` buffer. +/// So, the caller should not expect the `builder.output` buffer to contain anything. #[instrument(ret, skip_all)] async fn try_kerberos_optimistic<'a>( kerberos: &'a mut Kerberos, diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index ad68a46c..592e1287 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -111,7 +111,7 @@ pub struct Negotiate { /// On the first `initialize_security_context` call we call the Kerberos `initialize_security_context` to get the initial token. /// This is an advanced mechanism for protocol negotiation. For example, if KDC does not work properly or time skew is too big, /// then we fallback to NTLM and continue the NLA. - /// But if the first token is generated successfully (i.e. Kerberos works properly), then we save this token and reuse on + /// But if the first token is generated successfully (e.g. Kerberos works properly), then we save this token and reuse on /// the second `initialize_security_context` call. first_kdc_token: Option>, } @@ -232,6 +232,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; @@ -243,6 +244,9 @@ 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(), @@ -262,6 +266,9 @@ 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()))); diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs index 339fa4f2..372e9128 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -357,6 +357,10 @@ fn spnego_kerberos_u2u() { }), 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. }), }, ); @@ -436,7 +440,8 @@ fn run_spnego( server_flags: ServerRequestFlags, steps: usize, get_network_client: impl Fn(KdcMock) -> Box, - package_list: Option, + client_package_list: Option, + server_package_list: Option, ) -> (SspiContext, SspiContext) { let KrbEnvironment { realm, @@ -475,7 +480,7 @@ fn run_spnego( let mut spnego_client = SspiContext::Negotiate( Negotiate::new_client(NegotiateConfig::new( Box::new(client_config.clone()), - package_list.clone(), + client_package_list.clone(), CLIENT_COMPUTER_NAME.into(), )) .unwrap(), @@ -502,7 +507,7 @@ fn run_spnego( Negotiate::new_server( NegotiateConfig::new( Box::new(kerberos_server_config), - package_list, + server_package_list.clone(), SERVER_COMPUTER_NAME.into(), ), vec![identity_1, identity_2], @@ -541,13 +546,15 @@ fn spnego_kerberos() { | 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, 3, |kdc| Box::new(NetworkClientMock { kdc }), - Some(String::from("kerberos,!ntlm")), + package_list.clone(), + package_list, ); let SspiContext::Negotiate(negotiate) = client else { @@ -572,13 +579,15 @@ 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 }), - Some(String::from("kerberos,ntlm")), + package_list.clone(), + package_list, ); let SspiContext::Negotiate(negotiate) = client else { @@ -601,6 +610,7 @@ fn spnego_kerberos_ntlm_fallback() { | ServerRequestFlags::SEQUENCE_DETECT | ServerRequestFlags::REPLAY_DETECT | ServerRequestFlags::CONFIDENTIALITY; + let package_list = Some(String::from("kerberos,ntlm")); for kind in [ ErrorKind::TimeSkew, @@ -612,7 +622,8 @@ fn spnego_kerberos_ntlm_fallback() { server_flags, 4, |_| Box::new(FailedNetworkClientMock { kind }), - Some(String::from("kerberos,ntlm")), + package_list.clone(), + package_list.clone(), ); let SspiContext::Negotiate(negotiate) = client else { @@ -626,3 +637,35 @@ fn spnego_kerberos_ntlm_fallback() { ); } } + +#[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, + ); + + 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(_)),); +} From d5d4e7ddc12731eca0d427895a857939e80ce055 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Fri, 13 Mar 2026 19:11:41 +0200 Subject: [PATCH 09/24] test(sspi): more tests and validations; --- src/negotiate/mod.rs | 5 + .../kerberos/context_validator.rs | 127 ++++++++++++++++++ tests/sspi/client_server/kerberos/mod.rs | 18 ++- tests/sspi/client_server/mod.rs | 2 +- 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 tests/sspi/client_server/kerberos/context_validator.rs diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 592e1287..7068cfe1 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -187,6 +187,11 @@ impl Negotiate { self.protocol.protocol_name() } + #[cfg(feature = "__test-data")] + pub fn first_krb_token(&self) -> Option<&[u8]> { + self.first_kdc_token.as_deref() + } + fn set_auth_identity(&mut self) -> Result<()> { let NegotiateMode::Server(auth_data) = &self.mode else { return Err(Error::new( 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..4060f0c6 --- /dev/null +++ b/tests/sspi/client_server/kerberos/context_validator.rs @@ -0,0 +1,127 @@ +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 { + pub u2u: bool, +} + +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(_) + )); + + match step { + 0 => { + if self.u2u { + assert!( + negotiate.first_krb_token().is_none(), + "When Kerberos U2U is used, it's impossible to reuse the preflight Kerberos token" + ); + } else { + assert!( + negotiate.first_krb_token().is_some(), + "When Kerberos U2U is not used, it's possible to reuse the preflight Kerberos token" + ); + } + } + 1 => { + assert!( + negotiate.first_krb_token().is_none(), + "After the second SPNEGO client call, the preflight Kerberos token must be `None`" + ); + } + _ => {} + } + } +} + +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(_))); + + match step { + 0 => { + assert!( + negotiate.first_krb_token().is_none(), + "The Kerberos preflight token must be `None` when KDC is not available, and SPNEGO must fallback to NTLM" + ); + } + 1 => { + assert!(negotiate.first_krb_token().is_none(),); + } + _ => {} + } + } +} + +pub(super) struct SpnegoServerNtlmFallbackValidator { + pub u2u: bool, +} + +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 => { + if self.u2u { + assert!( + negotiate.first_krb_token().is_none(), + "When Kerberos U2U is used, it's impossible to reuse the preflight Kerberos token" + ); + } else { + assert!( + negotiate.first_krb_token().is_some(), + "When Kerberos U2U is not used, it's possible to reuse the preflight Kerberos token" + ); + } + + assert!(matches!( + negotiate.negotiated_protocol(), + NegotiatedProtocol::Kerberos(_) + )); + } + 1 => { + if self.u2u { + assert!(negotiate.first_krb_token().is_none(),); + } else { + // The preflight Kerberos token was not reused because the server fallback to NTLM. + assert!(negotiate.first_krb_token().is_some(),); + } + + 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 372e9128..4e7cc0a4 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; @@ -21,6 +22,10 @@ use sspi::{ }; 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, }; @@ -208,10 +213,11 @@ 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 { let (client_status, token) = initialize_security_context( client, client_credentials_handle, @@ -221,6 +227,8 @@ fn run_kerberos( network_client, ); + context_validator.validate_client(step, client); + if client_status == SecurityStatus::Ok { test_encryption(client, server); test_stream_buffer_encryption(client, server); @@ -327,6 +335,7 @@ fn kerberos_auth() { server_flags, &mut network_client, 2, + EmptySspiContextValidator, ); } @@ -432,6 +441,7 @@ fn spnego_kerberos_u2u() { server_flags, &mut network_client, 3, + SpnegoKerberosContextValidator { u2u: true }, ); } @@ -442,6 +452,7 @@ fn run_spnego( get_network_client: impl Fn(KdcMock) -> Box, client_package_list: Option, server_package_list: Option, + context_validator: impl SspiContextValidator, ) -> (SspiContext, SspiContext) { let KrbEnvironment { realm, @@ -529,6 +540,7 @@ fn run_spnego( server_flags, &mut *network_client, steps, + context_validator, ); (spnego_client, spnego_server) @@ -555,6 +567,7 @@ fn spnego_kerberos() { |kdc| Box::new(NetworkClientMock { kdc }), package_list.clone(), package_list, + SpnegoKerberosContextValidator { u2u: false }, ); let SspiContext::Negotiate(negotiate) = client else { @@ -588,6 +601,7 @@ fn spnego_kerberos_dce_style() { |kdc| Box::new(NetworkClientMock { kdc }), package_list.clone(), package_list, + SpnegoKerberosContextValidator { u2u: false }, ); let SspiContext::Negotiate(negotiate) = client else { @@ -624,6 +638,7 @@ fn spnego_kerberos_ntlm_fallback() { |_| Box::new(FailedNetworkClientMock { kind }), package_list.clone(), package_list.clone(), + SpnegoKerberosNtlmFallbackValidator, ); let SspiContext::Negotiate(negotiate) = client else { @@ -660,6 +675,7 @@ fn spnego_kerberos_server_ntlm_fallback() { |kdc| Box::new(NetworkClientMock { kdc }), client_package_list, server_package_list, + SpnegoServerNtlmFallbackValidator { u2u: false }, ); let SspiContext::Negotiate(negotiate) = client else { diff --git a/tests/sspi/client_server/mod.rs b/tests/sspi/client_server/mod.rs index 1ac75bb7..d45864cf 100644 --- a/tests/sspi/client_server/mod.rs +++ b/tests/sspi/client_server/mod.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "network_client")] // The network_client feature is required for the client_server tests. +#![cfg(all(feature = "network_client", feature = "__test-data"))] // The network_client feature is required for the client_server tests. mod credssp; mod kerberos; From 1214f4bf5db530bc24b2f5db09c493e59e69506a Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Mon, 16 Mar 2026 20:04:35 +0200 Subject: [PATCH 10/24] refactor: small refactoring; --- src/lib.rs | 2 ++ src/negotiate/client.rs | 14 +++++++--- src/utils.rs | 3 ++- .../kerberos/context_validator.rs | 26 +++++++++---------- tests/sspi/client_server/kerberos/mod.rs | 10 +++---- .../client_server/kerberos/network_client.rs | 2 +- 6 files changed, 30 insertions(+), 27 deletions(-) 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 89dc9d01..7d2dec21 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -17,15 +17,21 @@ use crate::{ /// 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. +/// - [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. +/// 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. /// For example, the network client cannot connect to the KDC, or the KDC proxy returns an error. -/// - ErrorKind::CertificateUnknown: The KDC proxy certificate is invalid. -const NTLM_FALLBACK_ERROR_KINDS: [ErrorKind; 3] = [ +/// - [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. /// 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 index 4060f0c6..f3424ded 100644 --- a/tests/sspi/client_server/kerberos/context_validator.rs +++ b/tests/sspi/client_server/kerberos/context_validator.rs @@ -57,6 +57,7 @@ impl SspiContextValidator for SpnegoKerberosContextValidator { } } +/// Validates tha the client correctly falls back to NTLM. pub(super) struct SpnegoKerberosNtlmFallbackValidator; impl SspiContextValidator for SpnegoKerberosNtlmFallbackValidator { @@ -67,21 +68,18 @@ impl SspiContextValidator for SpnegoKerberosNtlmFallbackValidator { assert!(matches!(negotiate.negotiated_protocol(), NegotiatedProtocol::Ntlm(_))); - match step { - 0 => { - assert!( - negotiate.first_krb_token().is_none(), - "The Kerberos preflight token must be `None` when KDC is not available, and SPNEGO must fallback to NTLM" - ); - } - 1 => { - assert!(negotiate.first_krb_token().is_none(),); - } - _ => {} + if step == 0 { + assert!( + negotiate.first_krb_token().is_none(), + "The Kerberos preflight token must be `None` and SPNEGO must fallback to NTLM" + ); } + + 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 { pub u2u: bool, } @@ -113,10 +111,10 @@ impl SspiContextValidator for SpnegoServerNtlmFallbackValidator { } 1 => { if self.u2u { - assert!(negotiate.first_krb_token().is_none(),); + assert!(negotiate.first_krb_token().is_none()); } else { - // The preflight Kerberos token was not reused because the server fallback to NTLM. - assert!(negotiate.first_krb_token().is_some(),); + // The preflight Kerberos token was not reused because the server fallback to NTLM. + assert!(negotiate.first_krb_token().is_some()); } 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 4e7cc0a4..eacc0d6b 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -16,8 +16,8 @@ use sspi::credssp::SspiContext; use sspi::kerberos::ServerProperties; use sspi::network_client::NetworkClient; use sspi::{ - AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, ErrorKind, - Kerberos, KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, NegotiatedProtocol, SecurityBuffer, + AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, Kerberos, + KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, NegotiatedProtocol, SecurityBuffer, SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, }; use url::Url; @@ -626,11 +626,7 @@ fn spnego_kerberos_ntlm_fallback() { | ServerRequestFlags::CONFIDENTIALITY; let package_list = Some(String::from("kerberos,ntlm")); - for kind in [ - ErrorKind::TimeSkew, - ErrorKind::NoAuthenticatingAuthority, - ErrorKind::CertificateUnknown, - ] { + for kind in sspi::FALLBACK_ERROR_KINDS { let (client, _server) = run_spnego( client_flags, server_flags, diff --git a/tests/sspi/client_server/kerberos/network_client.rs b/tests/sspi/client_server/kerberos/network_client.rs index e0ced2f3..6617cdc2 100644 --- a/tests/sspi/client_server/kerberos/network_client.rs +++ b/tests/sspi/client_server/kerberos/network_client.rs @@ -48,6 +48,6 @@ pub(crate) struct FailedNetworkClientMock { impl NetworkClient for FailedNetworkClientMock { fn send(&self, _request: &NetworkRequest) -> Result> { - Err(sspi::Error::new(self.kind, "error from mock network client".to_owned())) + Err(sspi::Error::new(self.kind, "error from mock network client")) } } From ea7c1261f981f2ab86b434d463ef728c5e237c99 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Mon, 16 Mar 2026 20:40:59 +0200 Subject: [PATCH 11/24] refactor: small refactoring; --- src/negotiate/mod.rs | 4 ++-- tests/sspi/client_server/kerberos/context_validator.rs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 7068cfe1..f05314ec 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -111,7 +111,7 @@ pub struct Negotiate { /// On the first `initialize_security_context` call we call the Kerberos `initialize_security_context` to get the initial token. /// This is an advanced mechanism for protocol negotiation. For example, if KDC does not work properly or time skew is too big, /// then we fallback to NTLM and continue the NLA. - /// But if the first token is generated successfully (e.g. Kerberos works properly), then we save this token and reuse on + /// But if the first token is generated successfully (e.g. Kerberos works properly), then we save this token and reuse it on /// the second `initialize_security_context` call. first_kdc_token: Option>, } @@ -271,7 +271,7 @@ impl Negotiate { )); } - // We disable Kerberos completely when the target server has selected NTLm. + // We disable Kerberos completely when the target server has selected NTLM. self.package_list.kerberos = false; if self.protocol_name() != ntlm::PKG_NAME { diff --git a/tests/sspi/client_server/kerberos/context_validator.rs b/tests/sspi/client_server/kerberos/context_validator.rs index f3424ded..d504d918 100644 --- a/tests/sspi/client_server/kerberos/context_validator.rs +++ b/tests/sspi/client_server/kerberos/context_validator.rs @@ -57,7 +57,7 @@ impl SspiContextValidator for SpnegoKerberosContextValidator { } } -/// Validates tha the client correctly falls back to NTLM. +/// Validates that the client correctly falls back to NTLM. pub(super) struct SpnegoKerberosNtlmFallbackValidator; impl SspiContextValidator for SpnegoKerberosNtlmFallbackValidator { @@ -74,8 +74,6 @@ impl SspiContextValidator for SpnegoKerberosNtlmFallbackValidator { "The Kerberos preflight token must be `None` and SPNEGO must fallback to NTLM" ); } - - assert!(matches!(negotiate.negotiated_protocol(), NegotiatedProtocol::Ntlm(_))); } } From 57ac290be4dbe2d3a95e8c45be8c74ce53e19062 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Mon, 16 Mar 2026 21:15:10 +0200 Subject: [PATCH 12/24] refactor: small refactoring; --- src/negotiate/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 7d2dec21..9170dc2c 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -19,9 +19,9 @@ use crate::{ /// 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. -/// For example, the network client cannot connect to the KDC, or the KDC proxy returns an error. /// - [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] = [ From 0e9316efbecdda029384e01d166873c395b9e19a Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Tue, 17 Mar 2026 13:27:09 +0200 Subject: [PATCH 13/24] refactor: small refactoring; --- src/negotiate/mod.rs | 2 ++ tests/sspi/client_server/mod.rs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index f05314ec..757eb762 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -273,6 +273,8 @@ impl Negotiate { // We disable Kerberos completely when the target server has selected NTLM. self.package_list.kerberos = false; + // Clear any cached Kerberos token since Kerberos is now disabled and will not be used. + self.first_kdc_token = None; if self.protocol_name() != ntlm::PKG_NAME { self.protocol = diff --git a/tests/sspi/client_server/mod.rs b/tests/sspi/client_server/mod.rs index d45864cf..8a131a6a 100644 --- a/tests/sspi/client_server/mod.rs +++ b/tests/sspi/client_server/mod.rs @@ -1,4 +1,5 @@ -#![cfg(all(feature = "network_client", feature = "__test-data"))] // 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; From 505f4b30798c80b2c89bafb32b981cf69e9e99eb Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Tue, 17 Mar 2026 13:37:27 +0200 Subject: [PATCH 14/24] feat(sspi): negotiate: accept `SecurityStatus::Ok` in `try_kerberos_optimistic`; --- src/negotiate/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 9170dc2c..1d5da2d2 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -378,7 +378,7 @@ async fn try_kerberos_optimistic<'a>( ) -> Result> { let result = kerberos.initialize_security_context_impl(yield_point, builder).await?; - if result.status != SecurityStatus::ContinueNeeded { + if result.status != SecurityStatus::ContinueNeeded && result.status != SecurityStatus::Ok { return Err(Error::new( ErrorKind::InternalError, format!("unexpected Kerberos status: {:?}", result.status), From 5b943af5359d0435d2cb85127fcdf9ce2a9f27c3 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Tue, 17 Mar 2026 13:38:41 +0200 Subject: [PATCH 15/24] refactor: fix typo; --- src/negotiate/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 1d5da2d2..dd147cc8 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -361,7 +361,7 @@ fn prepare_final_neg_token( /// Kerberos authentication (which performs AS and TGS exchanges). /// /// Returns the Kerberos token on success, or an error if the authentication fails. -/// The caller should handle the error kind and specially to fallback to NTLM in some cases (like `ErrorKind::TimeSkew`). +/// The caller should handle the error kind specially to fallback to NTLM in some cases (like `ErrorKind::TimeSkew`). /// /// The function will empty the output [BufferType::Token] buffer in the builder /// and return the Kerberos token as a plain `Vec` buffer. From e269a8d4e84e9fb0862af58e669b390dc800ac7c Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Tue, 17 Mar 2026 15:52:12 +0200 Subject: [PATCH 16/24] . --- src/negotiate/client.rs | 153 +++++++++++++++--------------------- src/negotiate/generators.rs | 5 +- src/negotiate/mod.rs | 18 +---- 3 files changed, 72 insertions(+), 104 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index dd147cc8..61bdd91f 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -106,8 +106,36 @@ pub(crate) async fn initialize_security_context<'a>( debug!(?sname); + 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 + } + }; + // Try optimistic Kerberos authentication if the negotiated protocol is Kerberos. - let first_krb_token = if let NegotiatedProtocol::Kerberos(kerberos) = &mut negotiate.protocol { + let first_token = if let NegotiatedProtocol::Kerberos(kerberos) = &mut negotiate.protocol { // We try to call `initialize_security_context` on the Kerberos security package. // If this call does not succeed we fallback to NTLM. But if this call succeeds, we can save the output Kerberos token // and reuse it on the second Negotiate `initialize_security_context` call. Also, we need to take Kerberos U2U into the account. @@ -117,12 +145,16 @@ pub(crate) async fn initialize_security_context<'a>( // So, we clone the Kerberos context if U2U is used. This way, the Kerberos state will not be in progress on the second `initialize_security_context` call, // and we can process normally. But if the U2U is not used, then we will reuse the token and the Kerberos state. let kerberos = if is_u2u { &mut kerberos.clone() } else { kerberos }; - match try_kerberos_optimistic(kerberos, yield_point, builder).await { + match try_optimistic(kerberos, yield_point, builder).await { Ok(token) => { debug!("Optimistic Kerberos authentication succeeded"); - // We cannot reuse the token if the U2U is used, because it does not contain a TGT ticket and `enc-tkt-in-skey` is not enabled. - if is_u2u { None } else { Some(token) } + // We cannot use the token if the U2U is used, because it does not contain a TGT ticket and `enc-tkt-in-skey` is not enabled. + if is_u2u { + None + } else { + Some(token) + } } Err(err) if NTLM_FALLBACK_ERROR_KINDS.contains(&err.error_type) => { warn!("Kerberos authentication failed with {err} error, attempting NTLM fallback."); @@ -144,7 +176,7 @@ pub(crate) async fn initialize_security_context<'a>( } else { None }; - negotiate.first_kdc_token = first_krb_token; + negotiate.first_kdc_token = None; let mech_types = generate_mech_type_list( matches!(&negotiate.protocol, NegotiatedProtocol::Kerberos(_)), @@ -156,6 +188,7 @@ pub(crate) async fn initialize_security_context<'a>( let encoded_neg_token_init = picky_asn1_der::to_vec(&generate_neg_token_init( sname.as_ref().map(|sname| sname.as_slice()), mech_types, + first_krb_token, )?)?; let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; @@ -184,14 +217,6 @@ pub(crate) async fn initialize_security_context<'a>( 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; let mech_type: String = (&selected_mech.0).into(); @@ -208,46 +233,31 @@ pub(crate) async fn initialize_security_context<'a>( input_token.buffer.clear(); } - let mut result = if matches!(negotiate.protocol, NegotiatedProtocol::Kerberos(_)) - && let Some(token) = negotiate.first_kdc_token.take() - { - debug!("Using Kerberos token from the first optimistic Kerberos call."); - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - output_token.buffer = token; - - InitializeSecurityContextResult { - status: SecurityStatus::ContinueNeeded, - flags: ClientResponseFlags::empty(), - expiry: None, - } - } else { - 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 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)?; + let result = pku2u.initialize_security_context_impl(&mut transformed_builder)?; - builder.output = mem::take(&mut transformed_builder.output); + 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)); + 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)?; + let result = ntlm.initialize_security_context_impl(&mut transformed_builder)?; - builder.output = mem::take(&mut transformed_builder.output); + builder.output = mem::take(&mut transformed_builder.output); - result - } + result } }; @@ -341,51 +351,18 @@ fn prepare_final_neg_token( let neg_token_targ = generate_final_neg_token_targ( neg_result, response_token, - Some( - negotiate - .protocol - .generate_mic_token(&negotiate.mech_types, crate::private::Sealed)?, - ), + // Some( + // negotiate + // .protocol + // .generate_mic_token(&negotiate.mech_types, crate::private::Sealed)?, + // ), + None, ); let encoded_final_neg_token_targ = picky_asn1_der::to_vec(&neg_token_targ)?; - output_token.buffer = encoded_final_neg_token_targ; + // output_token.buffer = encoded_final_neg_token_targ; + output_token.buffer = Vec::new(); Ok(()) } - -/// Attempts optimistic Kerberos authentication. -/// -/// This function calls Kerberos's `initialize_security_context_impl` to generate an initial -/// Kerberos authentication (which performs AS and TGS exchanges). -/// -/// Returns the Kerberos token on success, or an error if the authentication fails. -/// The caller should handle the error kind specially to fallback to NTLM in some cases (like `ErrorKind::TimeSkew`). -/// -/// The function will empty the output [BufferType::Token] buffer in the builder -/// and return the Kerberos token as a plain `Vec` buffer. -/// So, the caller should not expect the `builder.output` buffer to contain anything. -#[instrument(ret, skip_all)] -async fn try_kerberos_optimistic<'a>( - kerberos: &'a mut Kerberos, - yield_point: &mut YieldPointLocal, - builder: &'a mut crate::builders::FilledInitializeSecurityContext< - '_, - '_, - ::CredentialsHandle, - >, -) -> Result> { - let result = kerberos.initialize_security_context_impl(yield_point, builder).await?; - - if result.status != SecurityStatus::ContinueNeeded && result.status != SecurityStatus::Ok { - return Err(Error::new( - ErrorKind::InternalError, - format!("unexpected Kerberos status: {:?}", result.status), - )); - } - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - - Ok(mem::take(&mut output_token.buffer)) -} diff --git a/src/negotiate/generators.rs b/src/negotiate/generators.rs index d35c9b07..83d65782 100644 --- a/src/negotiate/generators.rs +++ b/src/negotiate/generators.rs @@ -48,8 +48,11 @@ pub(super) fn generate_mech_type_list(kerberos: bool, ntlm: bool) -> Result, mech_list: MechTypeList, + first_krb_token: Option>, ) -> Result> { - let mech_token = if let Some(sname) = sname { + let mech_token = if let Some(token) = first_krb_token { + Some(ExplicitContextTag2::from(OctetStringAsn1::from(token))) + } else if let Some(sname) = sname { let sname = sname .iter() .map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string(sname.to_string())?))) diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 757eb762..a0d9df95 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -106,14 +106,10 @@ pub struct Negotiate { client_computer_name: String, mode: NegotiateMode, /// Encoded [MechTypeList]. Used for `mechListMIC` token verification. - mech_types: Vec, + /// + /// `mechListMIC` token verification is optional. If this field is `None` then no verification will be performed. + mech_types: Option>, mic_verified: bool, - /// On the first `initialize_security_context` call we call the Kerberos `initialize_security_context` to get the initial token. - /// This is an advanced mechanism for protocol negotiation. For example, if KDC does not work properly or time skew is too big, - /// then we fallback to NTLM and continue the NLA. - /// But if the first token is generated successfully (e.g. Kerberos works properly), then we save this token and reuse it on - /// the second `initialize_security_context` call. - first_kdc_token: Option>, } #[derive(Clone, Copy, Debug)] @@ -179,7 +175,6 @@ impl Negotiate { mode, mech_types: Default::default(), mic_verified: false, - first_kdc_token: None, }) } @@ -187,11 +182,6 @@ impl Negotiate { self.protocol.protocol_name() } - #[cfg(feature = "__test-data")] - pub fn first_krb_token(&self) -> Option<&[u8]> { - self.first_kdc_token.as_deref() - } - fn set_auth_identity(&mut self) -> Result<()> { let NegotiateMode::Server(auth_data) = &self.mode else { return Err(Error::new( @@ -273,8 +263,6 @@ impl Negotiate { // We disable Kerberos completely when the target server has selected NTLM. self.package_list.kerberos = false; - // Clear any cached Kerberos token since Kerberos is now disabled and will not be used. - self.first_kdc_token = None; if self.protocol_name() != ntlm::PKG_NAME { self.protocol = From c8dab39dc3111ce1588ee4a16c7fbd9ed392a225 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Wed, 18 Mar 2026 12:50:15 +0200 Subject: [PATCH 17/24] . --- src/kerberos/client/generators.rs | 20 ++- src/kerberos/client/mod.rs | 52 +++++++- src/kerberos/mod.rs | 5 +- src/kerberos/server/mod.rs | 3 + src/negotiate/client.rs | 200 +++++++++++------------------- src/negotiate/generators.rs | 41 ++---- src/negotiate/mod.rs | 80 ++++++++++-- 7 files changed, 221 insertions(+), 180 deletions(-) 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..23f8a2b3 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, utf16_bytes_to_utf8_string}; 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/mod.rs b/src/kerberos/server/mod.rs index e62643c3..cc168ff5 100644 --- a/src/kerberos/server/mod.rs +++ b/src/kerberos/server/mod.rs @@ -108,6 +108,9 @@ pub async fn accept_security_context( let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; let status = match server.state { + KerberosState::TgtExchange => { + todo!() + } KerberosState::Preauthentication => { let ap_req = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { picky_asn1_der::from_bytes::(&input_token.buffer)? diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 61bdd91f..272f41b8 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -8,11 +8,9 @@ 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, Kerberos, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, SecurityStatus, - SspiImpl, + 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. @@ -86,97 +84,68 @@ pub(crate) async fn initialize_security_context<'a>( match negotiate.state { NegotiateState::Initial => { - let is_u2u = builder + if builder .context_requirements - .contains(ClientRequestFlags::USE_SESSION_KEY); - - let sname = if is_u2u { - 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]) + .contains(ClientRequestFlags::USE_SESSION_KEY) + { + negotiate.mic_needed = true; + negotiate.mic_verified = false; } else { - None - }; - - debug!(?sname); - - 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)?; + negotiate.mic_needed = false; + negotiate.mic_verified = true; + } - builder.output = mem::take(&mut transformed_builder.output); + 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), + )); + } - result - } - NegotiatedProtocol::Kerberos(kerberos) => { - kerberos.initialize_security_context_impl(yield_point, builder).await? + let token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + Some(mem::take(&mut token.buffer)) } - 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); + 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."); - result - } - }; + if !negotiate.fallback_to_ntlm() { + warn!("Failed to fallback to NTLM: NTLM is disabled."); - // Try optimistic Kerberos authentication if the negotiated protocol is Kerberos. - let first_token = if let NegotiatedProtocol::Kerberos(kerberos) = &mut negotiate.protocol { - // We try to call `initialize_security_context` on the Kerberos security package. - // If this call does not succeed we fallback to NTLM. But if this call succeeds, we can save the output Kerberos token - // and reuse it on the second Negotiate `initialize_security_context` call. Also, we need to take Kerberos U2U into the account. - // If the Kerberos U2U is used, then we cannot reuse the output Kerberos token, because it does not contain a TGT ticket - // and `enc-tkt-in-skey` is not enabled. - // - // So, we clone the Kerberos context if U2U is used. This way, the Kerberos state will not be in progress on the second `initialize_security_context` call, - // and we can process normally. But if the U2U is not used, then we will reuse the token and the Kerberos state. - let kerberos = if is_u2u { &mut kerberos.clone() } else { kerberos }; - match try_optimistic(kerberos, yield_point, builder).await { - Ok(token) => { - debug!("Optimistic Kerberos authentication succeeded"); - - // We cannot use the token if the U2U is used, because it does not contain a TGT ticket and `enc-tkt-in-skey` is not enabled. - if is_u2u { - None - } else { - Some(token) - } + return Err(err); } - Err(err) if 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."); + debug!("Fallback to NTLM succeeded"); - return Err(err); - } + let result = negotiate + .protocol + .initialize_security_context(negotiate.auth_identity.as_ref(), yield_point, builder) + .await?; - debug!("Fallback to NTLM succeeded"); - - None - } - Err(err) => { - return Err(err); + 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); } - } else { - None }; - negotiate.first_kdc_token = None; let mech_types = generate_mech_type_list( matches!(&negotiate.protocol, NegotiatedProtocol::Kerberos(_)), @@ -185,11 +154,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, - first_krb_token, - )?)?; + 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; @@ -211,7 +176,7 @@ 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, @@ -233,39 +198,16 @@ 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())?; - let neg_result = if negotiate.mic_verified { + let neg_result = if negotiate.mic_verified || !negotiate.mic_needed { result.status = SecurityStatus::Ok; negotiate.state = NegotiateState::Ok; @@ -277,7 +219,13 @@ 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(); + } + + prepare_neg_token(neg_result, negotiate, builder)?; } else { let token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; @@ -335,7 +283,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>, @@ -348,16 +296,16 @@ fn prepare_final_neg_token( None }; - let neg_token_targ = generate_final_neg_token_targ( - neg_result, - response_token, - // Some( - // negotiate - // .protocol - // .generate_mic_token(&negotiate.mech_types, crate::private::Sealed)?, - // ), - 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)?; diff --git a/src/negotiate/generators.rs b/src/negotiate/generators.rs index 83d65782..2c625d31 100644 --- a/src/negotiate/generators.rs +++ b/src/negotiate/generators.rs @@ -1,18 +1,17 @@ use oid::ObjectIdentifier; use picky::oids; -use picky_asn1::restricted_string::IA5String; use picky_asn1::wrapper::{ Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, IntegerAsn1, 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, TGT_REP_TOKEN_ID}; +use picky_krb::constants::types::TGT_REP_MSG_TYPE; +use picky_krb::data_types::Ticket; use picky_krb::gss_api::{ ApplicationTag0, GssApiNegInit, KrbMessage, MechType, MechTypeList, NegTokenInit, NegTokenTarg, NegTokenTarg1, }; -use picky_krb::messages::{TgtRep, TgtReq}; +use picky_krb::messages::TgtRep; use crate::{Error, ErrorKind, KERBEROS_VERSION, Result}; @@ -46,37 +45,10 @@ pub(super) fn generate_mech_type_list(kerberos: bool, ntlm: bool) -> Result, mech_list: MechTypeList, - first_krb_token: Option>, + mech_token: Option>, ) -> Result> { - let mech_token = if let Some(token) = first_krb_token { - Some(ExplicitContextTag2::from(OctetStringAsn1::from(token))) - } else 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()), @@ -127,6 +99,7 @@ pub(super) fn generate_neg_token_targ(mech_type: ObjectIdentifier, tgt_rep: Opti ))) }) .transpose()?; + Ok(NegTokenTarg1::from(NegTokenTarg { neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_INCOMPLETE.to_vec())))), supported_mech: Optional::from(Some(ExplicitContextTag1::from(MechType::from(mech_type)))), diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index a0d9df95..66d21f02 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::FilledInitializeSecurityContext; use crate::generator::{ GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal, }; @@ -22,9 +24,9 @@ use crate::ntlm::NtlmConfig; 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, + 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 +75,42 @@ impl NegotiatedProtocol { pub fn is_kerberos(&self) -> bool { matches!(self, NegotiatedProtocol::Kerberos(_)) } + + 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) + } + } + } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] enum NegotiateState { #[default] Initial, @@ -106,10 +141,14 @@ pub struct Negotiate { client_computer_name: String, mode: NegotiateMode, /// Encoded [MechTypeList]. Used for `mechListMIC` token verification. - /// - /// `mechListMIC` token verification is optional. If this field is `None` then no verification will be performed. - mech_types: Option>, + 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)] @@ -175,6 +214,7 @@ impl Negotiate { mode, mech_types: Default::default(), mic_verified: false, + mic_needed: true, }) } @@ -248,6 +288,16 @@ impl Negotiate { 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. + self.mic_needed = true; + self.mic_verified = false; } return Ok(()); @@ -267,6 +317,16 @@ impl Negotiate { if self.protocol_name() != ntlm::PKG_NAME { self.protocol = NegotiatedProtocol::Ntlm(Ntlm::with_config(NtlmConfig::new(self.client_computer_name.clone()))); + + // 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. + self.mic_needed = true; + self.mic_verified = false; } return Ok(()); @@ -448,8 +508,8 @@ impl<'a> Negotiate { 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 } } @@ -698,7 +758,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, From 4033ee7a43c5277c9efe2bb2c5d36b59ad00e4bd Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Wed, 18 Mar 2026 14:37:23 +0200 Subject: [PATCH 18/24] . --- src/negotiate/client.rs | 16 ++++++++++------ src/negotiate/mod.rs | 22 ++++++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 272f41b8..3801218f 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -204,8 +204,10 @@ pub(crate) async fn initialize_security_context<'a>( .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 || !negotiate.mic_needed { result.status = SecurityStatus::Ok; @@ -220,9 +222,13 @@ pub(crate) async fn initialize_security_context<'a>( }; 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 { + debug!(?server_neg_result); + debug!(?negotiate.state); + 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)?; @@ -308,9 +314,7 @@ fn prepare_neg_token( 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; - output_token.buffer = Vec::new(); + output_token.buffer = encoded_final_neg_token_targ; Ok(()) } diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index 66d21f02..bff16349 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -296,6 +296,8 @@ impl Negotiate { // > 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; } @@ -317,18 +319,18 @@ impl Negotiate { if self.protocol_name() != ntlm::PKG_NAME { self.protocol = NegotiatedProtocol::Ntlm(Ntlm::with_config(NtlmConfig::new(self.client_computer_name.clone()))); - - // 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. - self.mic_needed = true; - self.mic_verified = false; } + // [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(()); } From 7db8008f30348b1a321de0bb47b10a34b20f4a6d Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Wed, 18 Mar 2026 16:13:41 +0200 Subject: [PATCH 19/24] . --- src/kerberos/server/generators.rs | 14 +- src/kerberos/server/mod.rs | 74 +++++++++- src/negotiate/extractors.rs | 62 +++----- src/negotiate/generators.rs | 37 ++--- src/negotiate/mod.rs | 71 +++++++-- src/negotiate/server.rs | 136 +++++++----------- .../kerberos/context_validator.rs | 61 +------- tests/sspi/client_server/kerberos/mod.rs | 8 +- 8 files changed, 229 insertions(+), 234 deletions(-) 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 cc168ff5..313d62b9 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,10 +109,68 @@ 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)?; - let status = match server.state { - KerberosState::TgtExchange => { - todo!() + if server.state == KerberosState::TgtExchange { + if let Ok(tgt_req) = 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) = 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) { picky_asn1_der::from_bytes::(&input_token.buffer)? @@ -356,7 +416,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/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 2c625d31..fdb3a952 100644 --- a/src/negotiate/generators.rs +++ b/src/negotiate/generators.rs @@ -1,19 +1,16 @@ use oid::ObjectIdentifier; use picky::oids; 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}; -use picky_krb::constants::types::TGT_REP_MSG_TYPE; -use picky_krb::data_types::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; -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 { @@ -87,18 +84,12 @@ 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( + 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())))), @@ -107,11 +98,3 @@ pub(super) fn generate_neg_token_targ(mech_type: ObjectIdentifier, tgt_rep: Opti 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 bff16349..b0b2d95a 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -13,7 +13,7 @@ pub use config::{NegotiateConfig, ProtocolConfig}; use picky::oids; use picky_krb::gss_api::MechType; -use crate::builders::FilledInitializeSecurityContext; +use crate::builders::{EmptyAcceptSecurityContext, FilledAcceptSecurityContext, FilledInitializeSecurityContext}; use crate::generator::{ GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal, }; @@ -23,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, InitializeSecurityContextResult, 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"; @@ -108,6 +109,60 @@ impl NegotiatedProtocol { } } } + + 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, PartialEq)] @@ -502,8 +557,8 @@ 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 } @@ -751,7 +806,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 diff --git a/src/negotiate/server.rs b/src/negotiate/server.rs index f2673f3c..a8905e7b 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, Result, SecurityBuffer, SecurityStatus, + ServerResponseFlags, SspiImpl, }; /// Performs one authentication step. @@ -30,51 +27,50 @@ 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 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; + + // 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(mech_type, None)?)? + } else { + // The selected mech type is the most preferred one by client, so we can use the token sent by the client. + let response_token = if let Some(mut mech_token) = mech_token { + input_token.buffer = mem::take(&mut mech_token); - kerberos.krb5_user_to_user = true; + negotiate + .protocol + .accept_security_context(yield_point, &mut builder) + .await?; - 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(); + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - Some(generate_tgt_rep( - request_tgt(kerberos, &credentials, &tgt_req, yield_point).await?, - )) - } else { - None - }; + Some(mem::take(&mut output_token.buffer)) + } else { + None + }; - let mut encoded_neg_token_targ = picky_asn1_der::to_vec(&generate_neg_token_targ(mech_type, tgt_rep)?)?; + picky_asn1_der::to_vec(&generate_neg_token_targ(mech_type, response_token)?)? + }; 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; @@ -97,53 +93,17 @@ 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() { + let neg_result = if mech_list_mic.is_some() || !negotiate.mic_needed { negotiate.set_auth_identity()?; negotiate.verify_mic_token(mech_list_mic.as_deref())?; @@ -167,7 +127,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: _, @@ -218,15 +178,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/tests/sspi/client_server/kerberos/context_validator.rs b/tests/sspi/client_server/kerberos/context_validator.rs index d504d918..34dbed4b 100644 --- a/tests/sspi/client_server/kerberos/context_validator.rs +++ b/tests/sspi/client_server/kerberos/context_validator.rs @@ -17,12 +17,10 @@ impl SspiContextValidator for EmptySspiContextValidator { } /// Performs additional SPNEGO context validation for Kerberos over SPNEGO tests. -pub(super) struct SpnegoKerberosContextValidator { - pub u2u: bool, -} +pub(super) struct SpnegoKerberosContextValidator; impl SspiContextValidator for SpnegoKerberosContextValidator { - fn validate_client(&mut self, step: usize, client: &SspiContext) { + fn validate_client(&mut self, _step: usize, client: &SspiContext) { let SspiContext::Negotiate(negotiate) = client else { panic!("Expected Negotiate context"); }; @@ -31,29 +29,6 @@ impl SspiContextValidator for SpnegoKerberosContextValidator { negotiate.negotiated_protocol(), NegotiatedProtocol::Kerberos(_) )); - - match step { - 0 => { - if self.u2u { - assert!( - negotiate.first_krb_token().is_none(), - "When Kerberos U2U is used, it's impossible to reuse the preflight Kerberos token" - ); - } else { - assert!( - negotiate.first_krb_token().is_some(), - "When Kerberos U2U is not used, it's possible to reuse the preflight Kerberos token" - ); - } - } - 1 => { - assert!( - negotiate.first_krb_token().is_none(), - "After the second SPNEGO client call, the preflight Kerberos token must be `None`" - ); - } - _ => {} - } } } @@ -61,26 +36,17 @@ impl SspiContextValidator for SpnegoKerberosContextValidator { pub(super) struct SpnegoKerberosNtlmFallbackValidator; impl SspiContextValidator for SpnegoKerberosNtlmFallbackValidator { - fn validate_client(&mut self, step: usize, client: &SspiContext) { + 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(_))); - - if step == 0 { - assert!( - negotiate.first_krb_token().is_none(), - "The Kerberos preflight token must be `None` and SPNEGO must fallback to NTLM" - ); - } } } /// Validates that the client correctly falls back to NTLM when the server selected NTLM in SPNEGO instead of Kerberos. -pub(super) struct SpnegoServerNtlmFallbackValidator { - pub u2u: bool, -} +pub(super) struct SpnegoServerNtlmFallbackValidator; impl SspiContextValidator for SpnegoServerNtlmFallbackValidator { fn validate_client(&mut self, step: usize, client: &SspiContext) { @@ -90,31 +56,12 @@ impl SspiContextValidator for SpnegoServerNtlmFallbackValidator { match step { 0 => { - if self.u2u { - assert!( - negotiate.first_krb_token().is_none(), - "When Kerberos U2U is used, it's impossible to reuse the preflight Kerberos token" - ); - } else { - assert!( - negotiate.first_krb_token().is_some(), - "When Kerberos U2U is not used, it's possible to reuse the preflight Kerberos token" - ); - } - assert!(matches!( negotiate.negotiated_protocol(), NegotiatedProtocol::Kerberos(_) )); } 1 => { - if self.u2u { - assert!(negotiate.first_krb_token().is_none()); - } else { - // The preflight Kerberos token was not reused because the server fallback to NTLM. - assert!(negotiate.first_krb_token().is_some()); - } - 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 eacc0d6b..0f2faaa5 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -441,7 +441,7 @@ fn spnego_kerberos_u2u() { server_flags, &mut network_client, 3, - SpnegoKerberosContextValidator { u2u: true }, + SpnegoKerberosContextValidator, ); } @@ -567,7 +567,7 @@ fn spnego_kerberos() { |kdc| Box::new(NetworkClientMock { kdc }), package_list.clone(), package_list, - SpnegoKerberosContextValidator { u2u: false }, + SpnegoKerberosContextValidator, ); let SspiContext::Negotiate(negotiate) = client else { @@ -601,7 +601,7 @@ fn spnego_kerberos_dce_style() { |kdc| Box::new(NetworkClientMock { kdc }), package_list.clone(), package_list, - SpnegoKerberosContextValidator { u2u: false }, + SpnegoKerberosContextValidator, ); let SspiContext::Negotiate(negotiate) = client else { @@ -671,7 +671,7 @@ fn spnego_kerberos_server_ntlm_fallback() { |kdc| Box::new(NetworkClientMock { kdc }), client_package_list, server_package_list, - SpnegoServerNtlmFallbackValidator { u2u: false }, + SpnegoServerNtlmFallbackValidator, ); let SspiContext::Negotiate(negotiate) = client else { From aee8b19f52425f69587b8283a94d0d8bf1d12dec Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 19 Mar 2026 15:52:49 +0200 Subject: [PATCH 20/24] . --- src/credssp/mod.rs | 4 +- src/kerberos/server/mod.rs | 12 ++- src/negotiate/client.rs | 13 ++- src/negotiate/generators.rs | 3 +- src/negotiate/mod.rs | 4 + src/negotiate/server.rs | 106 ++++++++++++++++++----- tests/sspi/client_server/kerberos/mod.rs | 12 ++- tests/sspi/client_server/negotiate.rs | 20 +++-- 8 files changed, 137 insertions(+), 37 deletions(-) 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/server/mod.rs b/src/kerberos/server/mod.rs index 313d62b9..a3bd8661 100644 --- a/src/kerberos/server/mod.rs +++ b/src/kerberos/server/mod.rs @@ -110,7 +110,11 @@ pub async fn accept_security_context( let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; if server.state == KerberosState::TgtExchange { - if let Ok(tgt_req) = decode_krb_message::(&input_token.buffer, TGT_REQ_TOKEN_ID) { + 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 @@ -157,7 +161,11 @@ pub async fn accept_security_context( flags: ServerResponseFlags::empty(), expiry: None, }); - } else if let Ok(_ap_req) = decode_krb_message::(&input_token.buffer, AP_REQ_TOKEN_ID) { + } 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. diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 3801218f..d78139f9 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -203,13 +203,19 @@ pub(crate) async fn initialize_security_context<'a>( .initialize_security_context(negotiate.auth_identity.as_ref(), yield_point, builder) .await?; + println!("initialize_security_context result: {result:?}"); + println!( + "client: MIC needed: {}, MIC verified: {}", + negotiate.mic_needed, negotiate.mic_verified + ); + 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_verified || !negotiate.mic_needed { + let neg_result = if !negotiate.mic_needed || (negotiate.mic_needed && negotiate.mic_verified) { result.status = SecurityStatus::Ok; negotiate.state = NegotiateState::Ok; @@ -222,12 +228,16 @@ pub(crate) async fn initialize_security_context<'a>( }; let server_neg_result = server_neg_result.0.map(|neg_result| neg_result.0.0); + debug!(?server_neg_result); debug!(?negotiate.state); + 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(); + println!("{:?}", negotiate.state); + return Ok(result); } @@ -244,6 +254,7 @@ pub(crate) async fn initialize_security_context<'a>( Ok(result) } NegotiateState::VerifyMic => { + println!("Verifying MIC token..."); let input = builder .input .as_mut() diff --git a/src/negotiate/generators.rs b/src/negotiate/generators.rs index fdb3a952..8e77de96 100644 --- a/src/negotiate/generators.rs +++ b/src/negotiate/generators.rs @@ -85,6 +85,7 @@ pub(super) fn generate_final_neg_token_targ( } pub(super) fn generate_neg_token_targ( + neg_result: Vec, mech_type: ObjectIdentifier, response_token: Option>, ) -> Result { @@ -92,7 +93,7 @@ pub(super) fn generate_neg_token_targ( 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), diff --git a/src/negotiate/mod.rs b/src/negotiate/mod.rs index b0b2d95a..30bf8593 100644 --- a/src/negotiate/mod.rs +++ b/src/negotiate/mod.rs @@ -77,6 +77,10 @@ impl NegotiatedProtocol { 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>, diff --git a/src/negotiate/server.rs b/src/negotiate/server.rs index a8905e7b..46b62b33 100644 --- a/src/negotiate/server.rs +++ b/src/negotiate/server.rs @@ -9,8 +9,8 @@ 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}; use crate::{ - AcceptSecurityContextResult, BufferType, Error, ErrorKind, Negotiate, Result, SecurityBuffer, SecurityStatus, - ServerResponseFlags, SspiImpl, + AcceptSecurityContextResult, BufferType, Error, ErrorKind, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, + SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, }; /// Performs one authentication step. @@ -35,6 +35,8 @@ pub(crate) async fn accept_security_context( 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. // @@ -47,34 +49,86 @@ pub(crate) async fn accept_security_context( 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(mech_type, None)?)? + 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, so we can use the token sent by the client. - let response_token = if let Some(mut mech_token) = mech_token { + // 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); - negotiate + let result = negotiate .protocol .accept_security_context(yield_point, &mut builder) .await?; + println!("(initial): accept_security_context result: {result:?}"); + + println!( + "server: MIC needed: {}, MIC verified: {}", + negotiate.mic_needed, negotiate.mic_verified + ); + let neg_result = + if result.status == SecurityStatus::Ok || result.status == SecurityStatus::CompleteNeeded { + if !negotiate.mic_needed || (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)) + (Some(mem::take(&mut output_token.buffer)), neg_result) } else { - None + (None, ACCEPT_INCOMPLETE) }; - picky_asn1_der::to_vec(&generate_neg_token_targ(mech_type, response_token)?)? + 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 { + 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; - negotiate.state = NegotiateState::InProgress; - - SecurityStatus::ContinueNeeded + status } NegotiateState::InProgress => { let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?; @@ -99,21 +153,31 @@ pub(crate) async fn accept_security_context( .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() || !negotiate.mic_needed { + 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; + } + + println!( + "server: MIC needed: {}, MIC verified: {}", + negotiate.mic_needed, negotiate.mic_verified + ); + let neg_result = if !negotiate.mic_needed || (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)?; @@ -150,10 +214,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", )); } }; @@ -165,7 +229,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>, diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs index 0f2faaa5..c3750655 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -218,6 +218,7 @@ fn run_kerberos( let mut client_in_token = Vec::new(); for step in 0..steps { + println!("----------------------"); let (client_status, token) = initialize_security_context( client, client_credentials_handle, @@ -227,9 +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); @@ -239,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"); @@ -547,7 +557,7 @@ fn run_spnego( } #[test] -fn spnego_kerberos() { +fn spnego_kerberos_1() { let client_flags = ClientRequestFlags::MUTUAL_AUTH | ClientRequestFlags::INTEGRITY | ClientRequestFlags::SEQUENCE_DETECT 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); From d46139d96a6cc7f092e1fce0a55182ae1d3e4345 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 19 Mar 2026 15:58:34 +0200 Subject: [PATCH 21/24] . --- src/negotiate/client.rs | 2 +- src/negotiate/server.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index d78139f9..007c7340 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -215,7 +215,7 @@ pub(crate) async fn initialize_security_context<'a>( negotiate.verify_mic_token(mech_list_mic.as_deref())?; } - let neg_result = if !negotiate.mic_needed || (negotiate.mic_needed && negotiate.mic_verified) { + let neg_result = if !negotiate.mic_needed || negotiate.mic_verified { result.status = SecurityStatus::Ok; negotiate.state = NegotiateState::Ok; diff --git a/src/negotiate/server.rs b/src/negotiate/server.rs index 46b62b33..abeff653 100644 --- a/src/negotiate/server.rs +++ b/src/negotiate/server.rs @@ -10,7 +10,7 @@ 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::{ AcceptSecurityContextResult, BufferType, Error, ErrorKind, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, - SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, + SecurityStatus, ServerResponseFlags, SspiImpl, }; /// Performs one authentication step. @@ -85,7 +85,7 @@ pub(crate) async fn accept_security_context( ); let neg_result = if result.status == SecurityStatus::Ok || result.status == SecurityStatus::CompleteNeeded { - if !negotiate.mic_needed || (negotiate.mic_needed && negotiate.mic_verified) { + if !negotiate.mic_needed || negotiate.mic_verified { negotiate.state = NegotiateState::Ok; status = SecurityStatus::Ok; @@ -165,7 +165,7 @@ pub(crate) async fn accept_security_context( "server: MIC needed: {}, MIC verified: {}", negotiate.mic_needed, negotiate.mic_verified ); - let neg_result = if !negotiate.mic_needed || (negotiate.mic_needed && negotiate.mic_verified) { + let neg_result = if !negotiate.mic_needed || negotiate.mic_verified { negotiate.state = NegotiateState::Ok; result.status = SecurityStatus::Ok; From 45da5ed567cd29c3f4441ef69a88709a3ccdaa66 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 19 Mar 2026 16:59:21 +0200 Subject: [PATCH 22/24] . --- src/negotiate/client.rs | 1 + src/negotiate/server.rs | 4 ++-- tools/dpapi-cli-client/src/logging.rs | 2 +- tools/dpapi-cli-client/src/main.rs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 007c7340..8d9c4fef 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -87,6 +87,7 @@ pub(crate) async fn initialize_security_context<'a>( 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; diff --git a/src/negotiate/server.rs b/src/negotiate/server.rs index abeff653..0c009541 100644 --- a/src/negotiate/server.rs +++ b/src/negotiate/server.rs @@ -10,7 +10,7 @@ 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::{ AcceptSecurityContextResult, BufferType, Error, ErrorKind, Negotiate, NegotiatedProtocol, Result, SecurityBuffer, - SecurityStatus, ServerResponseFlags, SspiImpl, + SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, }; /// Performs one authentication step. @@ -120,7 +120,7 @@ pub(crate) async fn accept_security_context( } else { false }; - if is_kerberos_u2u { + if is_kerberos_u2u || builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { negotiate.mic_needed = true; negotiate.mic_verified = false; } 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; From d6fde46fbfad0afb3a1fb373489a328195766e9e Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 19 Mar 2026 18:53:39 +0200 Subject: [PATCH 23/24] . --- src/negotiate/client.rs | 9 --------- src/negotiate/server.rs | 9 --------- 2 files changed, 18 deletions(-) diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index 8d9c4fef..c8d7d6a2 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -204,12 +204,6 @@ pub(crate) async fn initialize_security_context<'a>( .initialize_security_context(negotiate.auth_identity.as_ref(), yield_point, builder) .await?; - println!("initialize_security_context result: {result:?}"); - println!( - "client: MIC needed: {}, MIC verified: {}", - negotiate.mic_needed, negotiate.mic_verified - ); - if result.status == SecurityStatus::Ok { if negotiate.mic_needed { let mech_list_mic = mech_list_mic.0.map(|token| token.0.0); @@ -237,8 +231,6 @@ pub(crate) async fn initialize_security_context<'a>( let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; output_token.buffer.clear(); - println!("{:?}", negotiate.state); - return Ok(result); } @@ -255,7 +247,6 @@ pub(crate) async fn initialize_security_context<'a>( Ok(result) } NegotiateState::VerifyMic => { - println!("Verifying MIC token..."); let input = builder .input .as_mut() diff --git a/src/negotiate/server.rs b/src/negotiate/server.rs index 0c009541..5fdf4ff8 100644 --- a/src/negotiate/server.rs +++ b/src/negotiate/server.rs @@ -77,12 +77,7 @@ pub(crate) async fn accept_security_context( .protocol .accept_security_context(yield_point, &mut builder) .await?; - println!("(initial): accept_security_context result: {result:?}"); - println!( - "server: MIC needed: {}, MIC verified: {}", - negotiate.mic_needed, negotiate.mic_verified - ); let neg_result = if result.status == SecurityStatus::Ok || result.status == SecurityStatus::CompleteNeeded { if !negotiate.mic_needed || negotiate.mic_verified { @@ -161,10 +156,6 @@ pub(crate) async fn accept_security_context( negotiate.mic_verified = true; } - println!( - "server: MIC needed: {}, MIC verified: {}", - negotiate.mic_needed, negotiate.mic_verified - ); let neg_result = if !negotiate.mic_needed || negotiate.mic_verified { negotiate.state = NegotiateState::Ok; result.status = SecurityStatus::Ok; From 454c830924132adf723f5be0d0c55be00e7ff750 Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Thu, 19 Mar 2026 19:36:43 +0200 Subject: [PATCH 24/24] refactor: small refactoring; --- src/kerberos/client/mod.rs | 2 +- src/negotiate/client.rs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/kerberos/client/mod.rs b/src/kerberos/client/mod.rs index 23f8a2b3..656561b2 100644 --- a/src/kerberos/client/mod.rs +++ b/src/kerberos/client/mod.rs @@ -32,7 +32,7 @@ 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, parse_target_name, utf16_bytes_to_utf8_string}; +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, diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index c8d7d6a2..18bb7bb2 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -224,9 +224,6 @@ pub(crate) async fn initialize_security_context<'a>( let server_neg_result = server_neg_result.0.map(|neg_result| neg_result.0.0); - debug!(?server_neg_result); - debug!(?negotiate.state); - 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();