Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6b91089
feat(sspi): negotiate: improve ntlm fallback;
PavloMyroniuk-apriorit Mar 11, 2026
a80a520
test(sspi): spnego: kerberos to ntlm fallback;
PavloMyroniuk-apriorit Mar 12, 2026
f7b1a0c
feat(sspi): negotiate: clarify some behavior in comments;
PavloMyroniuk-apriorit Mar 12, 2026
c7bcb71
refactor(tests): small refactoring;
PavloMyroniuk-apriorit Mar 12, 2026
d6334c0
refactor(tests): small refactoring;
PavloMyroniuk-apriorit Mar 12, 2026
c3e87a2
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 12, 2026
c826182
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 12, 2026
8f1fa77
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 13, 2026
d5d4e7d
test(sspi): more tests and validations;
PavloMyroniuk-apriorit Mar 13, 2026
1214f4b
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 16, 2026
ea7c126
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 16, 2026
57ac290
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 16, 2026
0e9316e
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 17, 2026
505f4b3
feat(sspi): negotiate: accept `SecurityStatus::Ok` in `try_kerberos_o…
PavloMyroniuk-apriorit Mar 17, 2026
5b943af
refactor: fix typo;
PavloMyroniuk-apriorit Mar 17, 2026
e269a8d
.
PavloMyroniuk-apriorit Mar 17, 2026
c8dab39
.
PavloMyroniuk-apriorit Mar 18, 2026
4033ee7
.
PavloMyroniuk-apriorit Mar 18, 2026
7db8008
.
PavloMyroniuk-apriorit Mar 18, 2026
aee8b19
.
PavloMyroniuk-apriorit Mar 19, 2026
d46139d
.
PavloMyroniuk-apriorit Mar 19, 2026
45da5ed
.
PavloMyroniuk-apriorit Mar 19, 2026
d6fde46
.
PavloMyroniuk-apriorit Mar 19, 2026
454c830
refactor: small refactoring;
PavloMyroniuk-apriorit Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/credssp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -634,7 +634,7 @@ impl<C: CredentialsProxy<AuthenticationData = AuthIdentity> + 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
Expand Down
20 changes: 18 additions & 2 deletions src/kerberos/client/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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};
Expand Down Expand Up @@ -153,6 +153,22 @@ fn matches_domain(domain: &str, mapping_domain: &str) -> bool {
}
}

pub(super) fn generate_tgt_req(sname: &[&str]) -> Result<TgtReq> {
let sname = sname
.iter()
.map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string(sname.to_string())?)))
.collect::<Result<Vec<_>>>()?;

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> {
Expand Down
52 changes: 46 additions & 6 deletions src/kerberos/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -27,11 +27,12 @@ use self::generators::{
};
use crate::channel_bindings::ChannelBindings;
use crate::generator::YieldPointLocal;
use crate::kerberos::client::generators::generate_tgt_req;
use crate::kerberos::messages::{decode_krb_message, generate_krb_message};
use crate::kerberos::pa_datas::{AsRepSessionKeyExtractor, AsReqPaDataOptions};
use crate::kerberos::utils::serialize_message;
use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EC, TGT_SERVICE_NAME};
use crate::utils::generate_random_symmetric_key;
use crate::utils::{generate_random_symmetric_key, parse_target_name};
use crate::{
BufferType, ClientRequestFlags, ClientResponseFlags, CredentialsBuffers, Error, ErrorKind,
InitializeSecurityContextResult, Kerberos, KerberosState, Result, SecurityBuffer, SecurityStatus, SspiImpl,
Expand All @@ -51,6 +52,47 @@ pub async fn initialize_security_context<'a>(
) -> Result<InitializeSecurityContextResult> {
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
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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),
Expand Down
5 changes: 3 additions & 2 deletions src/kerberos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub static PACKAGE_INFO: LazyLock<PackageInfo> = LazyLock::new(|| PackageInfo {

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KerberosState {
TgtExchange,
Preauthentication,
ApExchange,
Final,
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down
14 changes: 11 additions & 3 deletions src/kerberos/server/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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),
}
}
79 changes: 75 additions & 4 deletions src/kerberos/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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<'_, <Kerberos as SspiImpl>::CredentialsHandle>,
) -> Result<AcceptSecurityContextResult> {
let input = builder
Expand All @@ -107,6 +109,75 @@ pub async fn accept_security_context(
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?;
let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?;

if server.state == KerberosState::TgtExchange {
if let Ok(tgt_req) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
picky_asn1_der::from_bytes::<TgtReq>(&input_token.buffer).map_err(Error::from)
} else {
decode_krb_message::<TgtReq>(&input_token.buffer, TGT_REQ_TOKEN_ID)
} {
// The first token is TGT_REQ. It means that the client wants to perform Kerberos U2U.

if !builder
.context_requirements
.contains(ServerRequestFlags::USE_SESSION_KEY)
{
warn!(
"KRB5 U2U has been negotiated (requested by the client) but the USE_SESSION_KEY flag is not set."
);
}

server.krb5_user_to_user = true;

let credentials = server
.server
.as_ref()
.ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "Kerberos server configuration not present"))?
.user
.as_ref()
.ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "KRB5 U2U has been negotiated (requested by the client) but the user credentials are not preset in Kerberos server configuration"))?
.clone();

let tgt_rep = generate_tgt_rep(request_tgt(server, &credentials, &tgt_req, yield_point).await?);

let mech_id = if server.krb5_user_to_user {
oids::krb5_user_to_user()
} else {
oids::krb5()
};

let encoded_tgt_rep = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
picky_asn1_der::to_vec(&tgt_rep)?
} else {
generate_krb_message(mech_id, TGT_REP_TOKEN_ID, tgt_rep)?
};

let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
output_token.buffer = encoded_tgt_rep;

server.state = KerberosState::Preauthentication;

return Ok(AcceptSecurityContextResult {
status: SecurityStatus::ContinueNeeded,
flags: ServerResponseFlags::empty(),
expiry: None,
});
} else if let Ok(_ap_req) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
picky_asn1_der::from_bytes::<ApReq>(&input_token.buffer).map_err(Error::from)
} else {
decode_krb_message::<ApReq>(&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) {
Expand Down Expand Up @@ -353,7 +424,7 @@ pub async fn accept_security_context(

SecurityStatus::Ok
}
KerberosState::Final => {
KerberosState::Final | KerberosState::TgtExchange => {
return Err(Error::new(
ErrorKind::OutOfSequence,
format!("got wrong Kerberos state: {:?}", server.state),
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading
Loading