Skip to content

Commit ce028d8

Browse files
refactor(sspi): Kerberos client implementation (#435)
1 parent 8c0b18a commit ce028d8

File tree

17 files changed

+1614
-1291
lines changed

17 files changed

+1614
-1291
lines changed

Cargo.lock

Lines changed: 196 additions & 207 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/dpapi/src/rpc/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ impl<'a> AuthProvider<'a> {
247247
.with_context_requirements(
248248
// Warning: do not change these flags if you don't know what you are doing.
249249
// The absence or presence of some flags can break the RPC auth. For example,
250-
// if you enable the `ClientRequestFlags::USER_TO_USER`, then it will fail.
250+
// if you enable the `ClientRequestFlags::USE_SESSION_KEY`, then it will fail.
251251
ClientRequestFlags::MUTUAL_AUTH
252252
| ClientRequestFlags::INTEGRITY
253253
| ClientRequestFlags::USE_DCE_STYLE

src/credssp/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ impl CredSspClient {
309309
ts_request.nego_tokens = Some(output_token.remove(0).buffer);
310310

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

314314
let peer_version =
315315
self.context.as_ref().unwrap().peer_version.expect(
@@ -368,7 +368,7 @@ impl CredSspClient {
368368
.unwrap()
369369
.encrypt_ts_credentials(self.credentials_handle.as_ref().unwrap(), self.cred_ssp_mode)?,
370370
);
371-
info!("tscredentials has been written");
371+
debug!("tscredentials has been written");
372372

373373
self.state = CredSspState::Final;
374374

src/kerberos/client/as_exchange.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use picky_krb::data_types::{KrbResult, ResultExt};
2+
use picky_krb::messages::{AsRep, KdcReqBody};
3+
4+
use crate::generator::YieldPointLocal;
5+
use crate::kerberos::client::extractors::extract_salt_from_krb_error;
6+
use crate::kerberos::client::generators::generate_as_req;
7+
use crate::kerberos::pa_datas::AsReqPaDataOptions;
8+
use crate::kerberos::utils::serialize_message;
9+
use crate::{Error, ErrorKind, Kerberos, Result};
10+
11+
/// Performs AS exchange as specified in [RFC 4210: The Authentication Service Exchange](https://www.rfc-editor.org/rfc/rfc4120#section-3.1).
12+
pub async fn as_exchange(
13+
client: &mut Kerberos,
14+
yield_point: &mut YieldPointLocal,
15+
kdc_req_body: &KdcReqBody,
16+
mut pa_data_options: AsReqPaDataOptions<'_>,
17+
) -> Result<AsRep> {
18+
pa_data_options.with_pre_auth(false);
19+
let pa_datas = pa_data_options.generate()?;
20+
let as_req = generate_as_req(pa_datas, kdc_req_body.clone());
21+
22+
let response = client.send(yield_point, &serialize_message(&as_req)?).await?;
23+
24+
// first 4 bytes are message len. skipping them
25+
{
26+
if response.len() < 4 {
27+
return Err(Error::new(
28+
ErrorKind::InternalError,
29+
"the KDC reply message is too small: expected at least 4 bytes",
30+
));
31+
}
32+
33+
let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]);
34+
let as_rep: KrbResult<AsRep> = KrbResult::deserialize(&mut d)?;
35+
36+
if as_rep.is_ok() {
37+
error!("KDC replied with AS_REP to the AS_REQ without the encrypted timestamp. The KRB_ERROR expected.");
38+
39+
return Err(Error::new(
40+
ErrorKind::InvalidToken,
41+
"KDC server should not process AS_REQ without the pa-pac data",
42+
));
43+
}
44+
45+
if let Some(correct_salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? {
46+
debug!("salt extracted successfully from the KRB_ERROR");
47+
48+
pa_data_options.with_salt(correct_salt.as_bytes().to_vec());
49+
}
50+
}
51+
52+
pa_data_options.with_pre_auth(true);
53+
let pa_datas = pa_data_options.generate()?;
54+
55+
let as_req = generate_as_req(pa_datas, kdc_req_body.clone());
56+
57+
let response = client.send(yield_point, &serialize_message(&as_req)?).await?;
58+
59+
if response.len() < 4 {
60+
return Err(Error::new(
61+
ErrorKind::InternalError,
62+
"the KDC reply message is too small: expected at least 4 bytes",
63+
));
64+
}
65+
66+
// first 4 bytes are message len. skipping them
67+
let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]);
68+
69+
Ok(KrbResult::<AsRep>::deserialize(&mut d)?.inspect_err(|err| error!(?err, "AS exchange error"))?)
70+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use picky_krb::crypto::CipherSuite;
2+
use picky_krb::messages::KrbPrivMessage;
3+
use rand::rngs::OsRng;
4+
use rand::Rng;
5+
6+
use crate::builders::ChangePassword;
7+
use crate::generator::YieldPointLocal;
8+
use crate::kerberos::client::extractors::{
9+
extract_encryption_params_from_as_rep, extract_session_key_from_as_rep, extract_status_code_from_krb_priv_response,
10+
};
11+
use crate::kerberos::client::generators::{
12+
generate_as_req_kdc_body, generate_authenticator, generate_krb_priv_request, get_client_principal_name_type,
13+
get_client_principal_realm, EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, GenerateAuthenticatorOptions,
14+
};
15+
use crate::kerberos::pa_datas::AsReqPaDataOptions;
16+
use crate::kerberos::utils::{serialize_message, unwrap_hostname};
17+
use crate::kerberos::{client, CHANGE_PASSWORD_SERVICE_NAME, DEFAULT_ENCRYPTION_TYPE, KADMIN};
18+
use crate::utils::generate_random_symmetric_key;
19+
use crate::{ClientRequestFlags, Error, ErrorKind, Kerberos, Result};
20+
21+
/// [Kerberos Change Password and Set Password Protocols](https://datatracker.ietf.org/doc/html/rfc3244#section-2)
22+
/// "The service accepts requests on UDP port 464 and TCP port 464 as well."
23+
const KPASSWD_PORT: u16 = 464;
24+
25+
#[instrument(level = "debug", ret, fields(state = ?client.state), skip(client, change_password))]
26+
pub async fn change_password<'a>(
27+
client: &'a mut Kerberos,
28+
yield_point: &mut YieldPointLocal,
29+
change_password: ChangePassword<'a>,
30+
) -> Result<()> {
31+
let username = &change_password.account_name;
32+
let domain = &change_password.domain_name;
33+
let password = &change_password.old_password;
34+
35+
let salt = format!("{}{}", domain, username);
36+
37+
let cname_type = get_client_principal_name_type(username, domain);
38+
let realm = &get_client_principal_realm(username, domain);
39+
let hostname = unwrap_hostname(client.config.client_computer_name.as_deref())?;
40+
41+
let options = GenerateAsReqOptions {
42+
realm,
43+
username,
44+
cname_type,
45+
snames: &[KADMIN, CHANGE_PASSWORD_SERVICE_NAME],
46+
// 4 = size of u32
47+
nonce: &OsRng.gen::<u32>().to_ne_bytes(),
48+
hostname: &hostname,
49+
context_requirements: ClientRequestFlags::empty(),
50+
};
51+
let kdc_req_body = generate_as_req_kdc_body(&options)?;
52+
53+
let pa_data_options = AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions {
54+
password: password.as_ref(),
55+
salt: salt.as_bytes().to_vec(),
56+
enc_params: client.encryption_params.clone(),
57+
with_pre_auth: false,
58+
});
59+
60+
let as_rep = client::as_exchange(client, yield_point, &kdc_req_body, pa_data_options).await?;
61+
62+
debug!("AS exchange finished successfully.");
63+
64+
client.realm = Some(as_rep.0.crealm.0.to_string());
65+
66+
let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?;
67+
debug!(?encryption_type, "Negotiated encryption type");
68+
69+
client.encryption_params.encryption_type = Some(CipherSuite::try_from(usize::from(encryption_type))?);
70+
71+
let session_key = extract_session_key_from_as_rep(&as_rep, &salt, password.as_ref(), &client.encryption_params)?;
72+
73+
let seq_num = client.next_seq_number();
74+
75+
let enc_type = client
76+
.encryption_params
77+
.encryption_type
78+
.as_ref()
79+
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
80+
let authenticator_seb_key = generate_random_symmetric_key(enc_type, &mut OsRng);
81+
82+
let authenticator = generate_authenticator(GenerateAuthenticatorOptions {
83+
kdc_rep: &as_rep.0,
84+
seq_num: Some(seq_num),
85+
sub_key: Some(EncKey {
86+
key_type: enc_type.clone(),
87+
key_value: authenticator_seb_key,
88+
}),
89+
checksum: None,
90+
channel_bindings: client.channel_bindings.as_ref(),
91+
extensions: Vec::new(),
92+
})?;
93+
94+
let krb_priv = generate_krb_priv_request(
95+
as_rep.0.ticket.0,
96+
&session_key,
97+
change_password.new_password.as_ref().as_bytes(),
98+
&authenticator,
99+
&client.encryption_params,
100+
seq_num,
101+
&hostname,
102+
)?;
103+
104+
if let Some((_realm, mut kdc_url)) = client.get_kdc() {
105+
kdc_url
106+
.set_port(Some(KPASSWD_PORT))
107+
.map_err(|_| Error::new(ErrorKind::InvalidParameter, "Cannot set port for KDC URL"))?;
108+
109+
let response = client.send(yield_point, &serialize_message(&krb_priv)?).await?;
110+
trace!(?response, "Change password raw response");
111+
112+
if response.len() < 4 {
113+
return Err(Error::new(
114+
ErrorKind::InternalError,
115+
"the KDC reply message is too small: expected at least 4 bytes",
116+
));
117+
}
118+
119+
let krb_priv_response = KrbPrivMessage::deserialize(&response[4..]).map_err(|err| {
120+
Error::new(
121+
ErrorKind::InvalidToken,
122+
format!("cannot deserialize krb_priv_response: {:?}", err),
123+
)
124+
})?;
125+
126+
let result_status = extract_status_code_from_krb_priv_response(
127+
&krb_priv_response.krb_priv,
128+
&authenticator.0.subkey.0.as_ref().unwrap().0.key_value.0 .0,
129+
&client.encryption_params,
130+
)?;
131+
132+
if result_status != 0 {
133+
return Err(Error::new(
134+
ErrorKind::WrongCredentialHandle,
135+
format!("unsuccessful krb result code: {}. expected 0", result_status),
136+
));
137+
}
138+
} else {
139+
return Err(Error::new(
140+
ErrorKind::NoAuthenticatingAuthority,
141+
"no KDC server found".to_owned(),
142+
));
143+
}
144+
145+
Ok(())
146+
}

0 commit comments

Comments
 (0)