Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
72 changes: 71 additions & 1 deletion crates/kdc/src/as_exchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ fn validate_pa_data_timestamp(
.map_err(|_| KdcError::PreAuthFailed("unable to decode PaEncTsEnc timestamp value"))?;
let current = OffsetDateTime::now_utc();

if client_timestamp > current || current - client_timestamp > Duration::from_secs(max_time_skew) {
if (current - client_timestamp).abs() > Duration::from_secs(max_time_skew) {
return Err(KdcError::ClockSkew("invalid pa-data: clock skew too great"));
}

Expand Down Expand Up @@ -231,3 +231,73 @@ pub(super) fn handle_as_req(as_req: &AsReq, kdc_config: &KerberosServer) -> Resu
}),
}))
}

#[cfg(test)]
mod tests {
use picky_asn1::date::GeneralizedTime;
use picky_asn1::wrapper::ExplicitContextTag0;
use picky_krb::data_types::KerberosTime;

use super::*;

fn encrypted_timestamp(user: &DomainUser, client_time: OffsetDateTime) -> PaData {
let cipher = CipherSuite::Aes256CtsHmacSha196.cipher();
let key = cipher
.generate_key_from_password(user.password.as_bytes(), user.salt.as_bytes())
.unwrap();
let timestamp = PaEncTsEnc {
patimestamp: ExplicitContextTag0::from(KerberosTime::from(GeneralizedTime::from(client_time))),
pausec: Optional::from(None),
};
let encrypted = cipher
.encrypt(
&key,
PA_ENC_TIMESTAMP_KEY_USAGE,
&picky_asn1_der::to_vec(&timestamp).unwrap(),
)
.unwrap();

PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ENC_TIMESTAMP.to_vec())),
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(
picky_asn1_der::to_vec(&EncryptedData {
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![u8::from(
CipherSuite::Aes256CtsHmacSha196,
)])),
kvno: Optional::from(None),
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted)),
})
.unwrap(),
)),
}
}

#[test]
fn encrypted_timestamp_allows_skew_in_both_directions() {
let user = DomainUser {
username: "user@example.com".to_owned(),
password: "password".to_owned(),
salt: "EXAMPLE.COMuser".to_owned(),
};
let now = OffsetDateTime::now_utc();

for delta in [-200, 0, 200] {
let pa_data = encrypted_timestamp(&user, now + time::Duration::seconds(delta));
assert!(
validate_pa_data_timestamp(&user, 300, &[pa_data]).is_ok(),
"offset {delta}s"
);
}

for delta in [-400, 400] {
let pa_data = encrypted_timestamp(&user, now + time::Duration::seconds(delta));
assert!(
matches!(
validate_pa_data_timestamp(&user, 300, &[pa_data]),
Err(KdcError::ClockSkew(_))
),
"offset {delta}s"
);
}
}
}
76 changes: 59 additions & 17 deletions src/kerberos/client/as_exchange.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use picky_krb::constants::error_codes::KRB_AP_ERR_SKEW;
use picky_krb::data_types::{KrbResult, ResultExt};
use picky_krb::messages::{AsRep, KdcReqBody};
use picky_krb::messages::{AsRep, KdcReqBody, KrbError};
use time::{Duration, OffsetDateTime};

use crate::generator::YieldPointLocal;
use crate::kerberos::client::extractors::extract_salt_from_krb_error;
Expand All @@ -8,15 +10,42 @@ 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).
fn clock_offset_from_error(error: &KrbError, received_at: OffsetDateTime) -> Result<Duration> {
let seconds = OffsetDateTime::try_from(error.0.stime.0.0.clone())
.map_err(|_| Error::new(ErrorKind::InvalidToken, "KDC skew error has invalid server time"))?;
let usec = error.0.susec.0.0.as_slice();
if usec.len() > 4 {
return Err(Error::new(
ErrorKind::InvalidToken,
"KDC skew error has invalid microseconds",
));
}
let microseconds = usec.iter().fold(0_u32, |value, byte| (value << 8) | u32::from(*byte));
if microseconds > 999_999 {
return Err(Error::new(
ErrorKind::InvalidToken,
"KDC skew error has invalid microseconds",
));
}
let server_time = seconds
.checked_add(Duration::microseconds(i64::from(microseconds)))
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "KDC skew error has invalid server time"))?;
Ok(server_time - received_at)
}

/// Performs the AS exchange as specified in [RFC 4120, section 3.1](https://www.rfc-editor.org/rfc/rfc4120#section-3.1).
///
/// On a KDC clock-skew error, retries pre-authentication once using the server
/// time in the error. The offset is kept on the client context for subsequent
/// TGS and AP authenticators; other errors are returned without retrying.
pub(crate) 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 pa_datas = pa_data_options.generate(client.current_kdc_time()?)?;
let as_req = generate_as_req(pa_datas, kdc_req_body.clone());

let response = client.send(yield_point, &serialize_message(&as_req)?).await?;
Expand Down Expand Up @@ -50,21 +79,34 @@ pub(crate) async fn as_exchange(
}

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 mut retried_skew = false;
loop {
let pa_datas = pa_data_options.generate(client.current_kdc_time()?)?;
let as_req = generate_as_req(pa_datas, kdc_req_body.clone());
let response = client.send(yield_point, &serialize_message(&as_req)?).await?;
let received_at = OffsetDateTime::now_utc();

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",
));
}

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..]);
let as_rep: KrbResult<AsRep> = KrbResult::deserialize(&mut d)?;
match as_rep {
Ok(as_rep) => return Ok(as_rep),
Err(err) if !retried_skew && err.0.error_code.0 == KRB_AP_ERR_SKEW => {
client.clock_offset = clock_offset_from_error(&err, received_at)?;
retried_skew = true;
debug!(offset = ?client.clock_offset, "Retrying AS exchange with KDC clock offset");
}
Err(err) => {
error!(?err, "AS exchange error");
return Err(err.into());
}
}
}

// 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"))?)
}
28 changes: 16 additions & 12 deletions src/kerberos/client/change_password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::kerberos::client::extractors::{
};
use crate::kerberos::client::generators::{
EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, GenerateAuthenticatorOptions, generate_as_req_kdc_body,
generate_authenticator, generate_krb_priv_request,
generate_authenticator_at, generate_krb_priv_request,
};
use crate::kerberos::client::principal::{get_client_principal_name_type, get_client_principal_realm};
use crate::kerberos::pa_datas::AsReqPaDataOptions;
Expand Down Expand Up @@ -82,17 +82,21 @@ pub async fn change_password<'a>(
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
let authenticator_seb_key = generate_random_symmetric_key(enc_type, &mut rand);

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 now = client.current_kdc_time()?;
let authenticator = generate_authenticator_at(
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(),
},
now,
)?;

let krb_priv = generate_krb_priv_request(
as_rep.0.ticket.0,
Expand Down
59 changes: 55 additions & 4 deletions src/kerberos/client/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ pub struct GenerateAsPaDataOptions<'a> {

/// Build the PA-ENC-TIMESTAMP pre-auth value, encrypting the current time with
/// an already-derived long-term `key` of type `encryption_type`.
fn encode_enc_timestamp_pa_data(key: &[u8], encryption_type: &CipherSuite) -> Result<PaData> {
fn encode_enc_timestamp_pa_data(
key: &[u8],
encryption_type: &CipherSuite,
current_date: OffsetDateTime,
) -> Result<PaData> {
let cipher = encryption_type.cipher();

let current_date = OffsetDateTime::now_utc();
let microseconds = current_date.microsecond().min(MAX_MICROSECONDS);

let timestamp = PaEncTsEnc {
Expand Down Expand Up @@ -178,6 +181,13 @@ fn encode_pac_request_pa_data() -> Result<PaData> {

#[instrument(level = "trace", ret, skip_all, fields(options.salt, options.enc_params, options.with_pre_auth))]
pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions<'_>) -> Result<Vec<PaData>> {
generate_pa_datas_for_as_req_at(options, OffsetDateTime::now_utc())
}

pub(crate) fn generate_pa_datas_for_as_req_at(
options: &GenerateAsPaDataOptions<'_>,
timestamp: OffsetDateTime,
) -> Result<Vec<PaData>> {
let GenerateAsPaDataOptions {
password,
salt,
Expand All @@ -192,7 +202,7 @@ pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions<'_>) -> Re
let key = encryption_type
.cipher()
.generate_key_from_password(password.as_bytes(), salt)?;
pa_datas.push(encode_enc_timestamp_pa_data(&key, encryption_type)?);
pa_datas.push(encode_enc_timestamp_pa_data(&key, encryption_type, timestamp)?);
}

pa_datas.push(encode_pac_request_pa_data()?);
Expand All @@ -214,12 +224,20 @@ pub struct GenerateKeytabPaDataOptions {

#[instrument(level = "trace", ret, skip_all, fields(options.key_enctype, options.with_pre_auth))]
pub fn generate_pa_datas_for_as_req_with_key(options: &GenerateKeytabPaDataOptions) -> Result<Vec<PaData>> {
generate_pa_datas_for_as_req_with_key_at(options, OffsetDateTime::now_utc())
}

pub(crate) fn generate_pa_datas_for_as_req_with_key_at(
options: &GenerateKeytabPaDataOptions,
timestamp: OffsetDateTime,
) -> Result<Vec<PaData>> {
let mut pa_datas = Vec::new();

if options.with_pre_auth {
pa_datas.push(encode_enc_timestamp_pa_data(
options.key.as_ref(),
&options.key_enctype,
timestamp,
)?);
}

Expand Down Expand Up @@ -600,6 +618,13 @@ pub struct GenerateAuthenticatorOptions<'a> {
/// Generated ApReq Authenticator.
#[instrument(level = "trace", ret)]
pub fn generate_authenticator(options: GenerateAuthenticatorOptions<'_>) -> Result<Authenticator> {
generate_authenticator_at(options, OffsetDateTime::now_utc())
}

pub(crate) fn generate_authenticator_at(
options: GenerateAuthenticatorOptions<'_>,
current_date: OffsetDateTime,
) -> Result<Authenticator> {
let GenerateAuthenticatorOptions {
kdc_rep,
seq_num,
Expand All @@ -609,7 +634,6 @@ pub fn generate_authenticator(options: GenerateAuthenticatorOptions<'_>) -> Resu
..
} = options;

let current_date = OffsetDateTime::now_utc();
let mut microseconds = current_date.microsecond();
if microseconds > MAX_MICROSECONDS {
microseconds = MAX_MICROSECONDS;
Expand Down Expand Up @@ -970,4 +994,31 @@ mod tests {
// PA-ENC-TIMESTAMP plus PA-PAC-REQUEST.
assert_eq!(pa_datas.len(), 2);
}

#[test]
fn keytab_pa_data_uses_supplied_timestamp() {
let key = vec![0_u8; 32];
let timestamp = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap() + Duration::milliseconds(123);
let pa_datas = generate_pa_datas_for_as_req_with_key_at(
&GenerateKeytabPaDataOptions {
key: key.clone().into(),
key_enctype: CipherSuite::Aes256CtsHmacSha196,
with_pre_auth: true,
},
timestamp,
)
.unwrap();

let encrypted: EncryptedData = picky_asn1_der::from_bytes(&pa_datas[0].padata_data.0.0).unwrap();
let decrypted = CipherSuite::Aes256CtsHmacSha196
.cipher()
.decrypt(&key, PA_ENC_TIMESTAMP_KEY_USAGE, &encrypted.cipher.0.0)
.unwrap();
let decoded: PaEncTsEnc = picky_asn1_der::from_bytes(&decrypted).unwrap();
assert_eq!(
OffsetDateTime::try_from(decoded.patimestamp.0.0).unwrap(),
timestamp - Duration::milliseconds(123)
);
assert_eq!(decoded.pausec.0.unwrap().0.0, 123_000_u32.to_be_bytes());
}
}
25 changes: 15 additions & 10 deletions src/kerberos/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use self::extractors::{
use self::generators::{
ChecksumOptions, ChecksumValues, EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions,
GenerateAuthenticatorOptions, GenerateKeytabPaDataOptions, GenerateTgsReqOptions, GssFlags, generate_ap_rep,
generate_ap_req, generate_as_req_kdc_body, generate_authenticator, generate_tgs_req,
generate_ap_req, generate_as_req_kdc_body, generate_authenticator_at, generate_tgs_req,
};
use self::principal::{
ClientPrincipalName, get_client_principal_name, get_client_principal_name_type, get_client_principal_realm,
Expand Down Expand Up @@ -316,14 +316,18 @@ pub async fn initialize_security_context<'a>(
let mut hops = 0;

let (tgs_rep, session_key_2) = loop {
let mut authenticator = generate_authenticator(GenerateAuthenticatorOptions {
kdc_rep: &auth_rep,
seq_num: Some(rand.next_u32()),
sub_key: None,
checksum: None,
channel_bindings: client.channel_bindings.as_ref(),
extensions: Vec::new(),
})?;
let now = client.current_kdc_time()?;
let mut authenticator = generate_authenticator_at(
GenerateAuthenticatorOptions {
kdc_rep: &auth_rep,
seq_num: Some(rand.next_u32()),
sub_key: None,
checksum: None,
channel_bindings: client.channel_bindings.as_ref(),
extensions: Vec::new(),
},
now,
)?;

let tgs_req = generate_tgs_req(GenerateTgsReqOptions {
realm: &realm,
Expand Down Expand Up @@ -436,7 +440,8 @@ pub async fn initialize_security_context<'a>(
extensions: Vec::new(),
};

let authenticator = generate_authenticator(authenticator_options)?;
let now = client.current_kdc_time()?;
let authenticator = generate_authenticator_at(authenticator_options, now)?;

let ap_req = generate_ap_req(
tgs_rep.0.ticket.0,
Expand Down
Loading
Loading