Skip to content

refactor(sspi): Kerberos client implementation #435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
403 changes: 196 additions & 207 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/dpapi/src/rpc/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ impl<'a> AuthProvider<'a> {
.with_context_requirements(
// Warning: do not change these flags if you don't know what you are doing.
// The absence or presence of some flags can break the RPC auth. For example,
// if you enable the `ClientRequestFlags::USER_TO_USER`, then it will fail.
// if you enable the `ClientRequestFlags::USE_SESSION_KEY`, then it will fail.
ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::USE_DCE_STYLE
Expand Down
4 changes: 2 additions & 2 deletions src/credssp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ impl CredSspClient {
ts_request.nego_tokens = Some(output_token.remove(0).buffer);

if result.status == SecurityStatus::Ok {
info!("CredSSp finished NLA stage.");
debug!("CredSSp finished NLA stage.");

let peer_version =
self.context.as_ref().unwrap().peer_version.expect(
Expand Down Expand Up @@ -368,7 +368,7 @@ impl CredSspClient {
.unwrap()
.encrypt_ts_credentials(self.credentials_handle.as_ref().unwrap(), self.cred_ssp_mode)?,
);
info!("tscredentials has been written");
debug!("tscredentials has been written");

self.state = CredSspState::Final;

Expand Down
70 changes: 70 additions & 0 deletions src/kerberos/client/as_exchange.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use picky_krb::data_types::{KrbResult, ResultExt};
use picky_krb::messages::{AsRep, KdcReqBody};

use crate::generator::YieldPointLocal;
use crate::kerberos::client::extractors::extract_salt_from_krb_error;
use crate::kerberos::client::generators::generate_as_req;
use crate::kerberos::pa_datas::AsReqPaDataOptions;
use crate::kerberos::utils::serialize_message;
use crate::{Error, ErrorKind, Kerberos, Result};

/// Performs AS exchange as specified in [RFC 4210: The Authentication Service Exchange](https://www.rfc-editor.org/rfc/rfc4120#section-3.1).
pub async fn as_exchange(
client: &mut Kerberos,
yield_point: &mut YieldPointLocal,
kdc_req_body: &KdcReqBody,
mut pa_data_options: AsReqPaDataOptions<'_>,
) -> Result<AsRep> {
pa_data_options.with_pre_auth(false);
let pa_datas = pa_data_options.generate()?;
let as_req = generate_as_req(pa_datas, kdc_req_body.clone());

let response = client.send(yield_point, &serialize_message(&as_req)?).await?;

// first 4 bytes are message len. skipping them
{
if response.len() < 4 {
return Err(Error::new(
ErrorKind::InternalError,
"the KDC reply message is too small: expected at least 4 bytes",
));
}

let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]);
let as_rep: KrbResult<AsRep> = KrbResult::deserialize(&mut d)?;

if as_rep.is_ok() {
error!("KDC replied with AS_REP to the AS_REQ without the encrypted timestamp. The KRB_ERROR expected.");

return Err(Error::new(
ErrorKind::InvalidToken,
"KDC server should not process AS_REQ without the pa-pac data",
));
}

if let Some(correct_salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? {
debug!("salt extracted successfully from the KRB_ERROR");

pa_data_options.with_salt(correct_salt.as_bytes().to_vec());
}
}

pa_data_options.with_pre_auth(true);
let pa_datas = pa_data_options.generate()?;

let as_req = generate_as_req(pa_datas, kdc_req_body.clone());

let response = client.send(yield_point, &serialize_message(&as_req)?).await?;

if response.len() < 4 {
return Err(Error::new(
ErrorKind::InternalError,
"the KDC reply message is too small: expected at least 4 bytes",
));
}

// first 4 bytes are message len. skipping them
let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]);

Ok(KrbResult::<AsRep>::deserialize(&mut d)?.inspect_err(|err| error!(?err, "AS exchange error"))?)
}
146 changes: 146 additions & 0 deletions src/kerberos/client/change_password.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use picky_krb::crypto::CipherSuite;
use picky_krb::messages::KrbPrivMessage;
use rand::rngs::OsRng;
use rand::Rng;

use crate::builders::ChangePassword;
use crate::generator::YieldPointLocal;
use crate::kerberos::client::extractors::{
extract_encryption_params_from_as_rep, extract_session_key_from_as_rep, extract_status_code_from_krb_priv_response,
};
use crate::kerberos::client::generators::{
generate_as_req_kdc_body, generate_authenticator, generate_krb_priv_request, get_client_principal_name_type,
get_client_principal_realm, EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, GenerateAuthenticatorOptions,
};
use crate::kerberos::pa_datas::AsReqPaDataOptions;
use crate::kerberos::utils::{serialize_message, unwrap_hostname};
use crate::kerberos::{client, CHANGE_PASSWORD_SERVICE_NAME, DEFAULT_ENCRYPTION_TYPE, KADMIN};
use crate::utils::generate_random_symmetric_key;
use crate::{ClientRequestFlags, Error, ErrorKind, Kerberos, Result};

/// [Kerberos Change Password and Set Password Protocols](https://datatracker.ietf.org/doc/html/rfc3244#section-2)
/// "The service accepts requests on UDP port 464 and TCP port 464 as well."
const KPASSWD_PORT: u16 = 464;

#[instrument(level = "debug", ret, fields(state = ?client.state), skip(client, change_password))]
pub async fn change_password<'a>(
client: &'a mut Kerberos,
yield_point: &mut YieldPointLocal,
change_password: ChangePassword<'a>,
) -> Result<()> {
let username = &change_password.account_name;
let domain = &change_password.domain_name;
let password = &change_password.old_password;

let salt = format!("{}{}", domain, username);

let cname_type = get_client_principal_name_type(username, domain);
let realm = &get_client_principal_realm(username, domain);
let hostname = unwrap_hostname(client.config.client_computer_name.as_deref())?;

let options = GenerateAsReqOptions {
realm,
username,
cname_type,
snames: &[KADMIN, CHANGE_PASSWORD_SERVICE_NAME],
// 4 = size of u32
nonce: &OsRng.gen::<u32>().to_ne_bytes(),
hostname: &hostname,
context_requirements: ClientRequestFlags::empty(),
};
let kdc_req_body = generate_as_req_kdc_body(&options)?;

let pa_data_options = AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions {
password: password.as_ref(),
salt: salt.as_bytes().to_vec(),
enc_params: client.encryption_params.clone(),
with_pre_auth: false,
});

let as_rep = client::as_exchange(client, yield_point, &kdc_req_body, pa_data_options).await?;

debug!("AS exchange finished successfully.");

client.realm = Some(as_rep.0.crealm.0.to_string());

let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?;
debug!(?encryption_type, "Negotiated encryption type");

client.encryption_params.encryption_type = Some(CipherSuite::try_from(usize::from(encryption_type))?);

let session_key = extract_session_key_from_as_rep(&as_rep, &salt, password.as_ref(), &client.encryption_params)?;

let seq_num = client.next_seq_number();

let enc_type = client
.encryption_params
.encryption_type
.as_ref()
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
let authenticator_seb_key = generate_random_symmetric_key(enc_type, &mut OsRng);

let authenticator = generate_authenticator(GenerateAuthenticatorOptions {
kdc_rep: &as_rep.0,
seq_num: Some(seq_num),
sub_key: Some(EncKey {
key_type: enc_type.clone(),
key_value: authenticator_seb_key,
}),
checksum: None,
channel_bindings: client.channel_bindings.as_ref(),
extensions: Vec::new(),
})?;

let krb_priv = generate_krb_priv_request(
as_rep.0.ticket.0,
&session_key,
change_password.new_password.as_ref().as_bytes(),
&authenticator,
&client.encryption_params,
seq_num,
&hostname,
)?;

if let Some((_realm, mut kdc_url)) = client.get_kdc() {
kdc_url
.set_port(Some(KPASSWD_PORT))
.map_err(|_| Error::new(ErrorKind::InvalidParameter, "Cannot set port for KDC URL"))?;

let response = client.send(yield_point, &serialize_message(&krb_priv)?).await?;
trace!(?response, "Change password raw response");

if response.len() < 4 {
return Err(Error::new(
ErrorKind::InternalError,
"the KDC reply message is too small: expected at least 4 bytes",
));
}

let krb_priv_response = KrbPrivMessage::deserialize(&response[4..]).map_err(|err| {
Error::new(
ErrorKind::InvalidToken,
format!("cannot deserialize krb_priv_response: {:?}", err),
)
})?;

let result_status = extract_status_code_from_krb_priv_response(
&krb_priv_response.krb_priv,
&authenticator.0.subkey.0.as_ref().unwrap().0.key_value.0 .0,
&client.encryption_params,
)?;

if result_status != 0 {
return Err(Error::new(
ErrorKind::WrongCredentialHandle,
format!("unsuccessful krb result code: {}. expected 0", result_status),
));
}
} else {
return Err(Error::new(
ErrorKind::NoAuthenticatingAuthority,
"no KDC server found".to_owned(),
));
}

Ok(())
}
Loading