use log::{debug, trace};
use std::time::Duration;
use crate::auth::kerberos::crypto::{
compute_checksum, etype_from_i32, kerberos_decrypt, kerberos_encrypt, string_to_key_aes,
string_to_key_rc4, EncryptionType,
};
use crate::auth::kerberos::kdc::{send_to_kdc, KdcConfig};
use crate::auth::kerberos::messages::{
encode_ap_req, encode_as_req, encode_authenticator, encode_pa_enc_timestamp, encode_tgs_req,
encode_tgs_req_body, parse_enc_kdc_rep_part, parse_kdc_rep, parse_krb_error, EncryptedData,
PaData, PrincipalName, Ticket,
};
use crate::auth::spnego::{wrap_neg_token_init, OID_KERBEROS, OID_MS_KERBEROS};
use crate::error::{Error, Result};
const KEY_USAGE_PA_ENC_TIMESTAMP: u32 = 1;
const KEY_USAGE_AS_REP_ENC_PART: u32 = 3;
const KEY_USAGE_AP_REQ_AUTHENTICATOR: u32 = 7;
const KEY_USAGE_AP_REQ_AUTHENTICATOR_SPNEGO: u32 = 11;
const KEY_USAGE_TGS_REP_ENC_PART_SESSION_KEY: u32 = 8;
const KEY_USAGE_TGS_REP_ENC_PART_SUBKEY: u32 = 9;
const KDC_ERR_PREAUTH_REQUIRED: i32 = 25;
const PA_ENC_TIMESTAMP: i32 = 2;
const PA_ETYPE_INFO2: i32 = 19;
const PA_PAC_REQUEST: i32 = 128;
#[derive(Debug, Clone)]
pub struct KerberosCredentials {
pub username: String,
pub password: String,
pub realm: String,
pub kdc_address: String,
}
pub struct KerberosAuthenticator {
credentials: KerberosCredentials,
tgt: Option<Ticket>,
as_session_key: Option<Vec<u8>>,
service_ticket: Option<Ticket>,
tgs_session_key: Option<Vec<u8>>,
ap_req_bytes: Option<Vec<u8>>,
session_key: Option<Vec<u8>>,
etype: EncryptionType,
}
impl KerberosAuthenticator {
pub fn new(credentials: KerberosCredentials) -> Self {
Self {
credentials,
tgt: None,
as_session_key: None,
service_ticket: None,
tgs_session_key: None,
ap_req_bytes: None,
session_key: None,
etype: EncryptionType::Aes256CtsHmacSha196,
}
}
pub async fn authenticate(&mut self, server_hostname: &str) -> Result<()> {
let kdc_config = KdcConfig {
address: self.credentials.kdc_address.clone(),
timeout: Duration::from_secs(10),
};
debug!("kerberos: starting AS exchange");
self.as_exchange(&kdc_config).await?;
debug!(
"kerberos: starting TGS exchange for cifs/{}",
server_hostname
);
self.tgs_exchange(&kdc_config, server_hostname).await?;
debug!("kerberos: building AP-REQ");
self.build_ap_req()?;
debug!("kerberos: authentication complete");
Ok(())
}
pub async fn authenticate_from_ccache(
&mut self,
ccache: &crate::auth::kerberos::ccache::CCache,
server_hostname: &str,
) -> Result<()> {
let realm = &self.credentials.realm;
if let Some(svc) = ccache.find_service_ticket("cifs", server_hostname, realm) {
debug!(
"kerberos: using cached service ticket for cifs/{}",
server_hostname
);
self.load_service_ticket_from_ccache(svc)?;
self.build_ap_req()?;
debug!("kerberos: authentication complete (from cached service ticket)");
return Ok(());
}
if let Some(tgt_cred) = ccache.find_tgt(realm) {
debug!(
"kerberos: using cached TGT, doing TGS exchange for cifs/{}",
server_hostname
);
self.load_tgt_from_ccache(tgt_cred)?;
let kdc_config = KdcConfig {
address: self.credentials.kdc_address.clone(),
timeout: Duration::from_secs(10),
};
self.tgs_exchange(&kdc_config, server_hostname).await?;
self.build_ap_req()?;
debug!("kerberos: authentication complete (TGT from cache + TGS exchange)");
return Ok(());
}
Err(Error::Auth {
message: format!("ccache has no TGT or service ticket for realm {realm}"),
})
}
fn load_service_ticket_from_ccache(
&mut self,
cred: &crate::auth::kerberos::ccache::CcacheCredential,
) -> Result<()> {
let ticket = crate::auth::kerberos::messages::parse_ticket(&cred.ticket)?;
let etype = etype_from_code(cred.key_etype as i32)?;
self.etype = etype;
self.service_ticket = Some(ticket);
self.tgs_session_key = Some(cred.key_data.clone());
self.session_key = Some(cred.key_data.clone());
Ok(())
}
fn load_tgt_from_ccache(
&mut self,
cred: &crate::auth::kerberos::ccache::CcacheCredential,
) -> Result<()> {
let ticket = crate::auth::kerberos::messages::parse_ticket(&cred.ticket)?;
let etype = etype_from_code(cred.key_etype as i32)?;
self.etype = etype;
self.tgt = Some(ticket);
self.as_session_key = Some(cred.key_data.clone());
Ok(())
}
pub fn token(&self) -> Option<&[u8]> {
self.ap_req_bytes.as_deref()
}
pub fn session_key(&self) -> Option<&[u8]> {
self.session_key.as_deref()
}
async fn as_exchange(&mut self, kdc_config: &KdcConfig) -> Result<()> {
let realm = &self.credentials.realm;
let username = &self.credentials.username;
let cname = PrincipalName {
name_type: 1, name_string: vec![username.clone()],
};
let sname = PrincipalName {
name_type: 2, name_string: vec!["krbtgt".to_string(), realm.clone()],
};
let nonce = generate_nonce();
let etypes = [
EncryptionType::Aes256CtsHmacSha196,
EncryptionType::Aes128CtsHmacSha196,
EncryptionType::Rc4Hmac,
];
let as_req = encode_as_req(&cname, realm, &sname, nonce, &etypes, &[]);
let response = send_to_kdc(kdc_config, &as_req).await?;
trace!(
"kerberos: AS response first 32 bytes: {:02x?}",
&response[..response.len().min(32)]
);
let response = if !response.is_empty() && response[0] == 0x7e {
let krb_error = parse_krb_error(&response)?;
if krb_error.error_code == KDC_ERR_PREAUTH_REQUIRED {
debug!("kerberos: got KDC_ERR_PREAUTH_REQUIRED, retrying with pre-authentication");
let chosen_etype = if let Some(ref e_data) = krb_error.e_data {
self.extract_best_etype(e_data).unwrap_or(self.etype)
} else {
self.etype
};
self.etype = chosen_etype;
let user_key = self.derive_user_key();
let (ctime, cusec) = current_kerberos_time();
let timestamp_plaintext = encode_pa_enc_timestamp(&ctime, cusec);
let encrypted_timestamp = kerberos_encrypt(
&user_key,
KEY_USAGE_PA_ENC_TIMESTAMP,
×tamp_plaintext,
self.etype,
);
let enc_timestamp_data = EncryptedData {
etype: self.etype as i32,
kvno: None,
cipher: encrypted_timestamp,
};
let pa_enc_ts_value = encode_encrypted_data_raw(&enc_timestamp_data);
let pa_pac_value = encode_pa_pac_request(true);
let padata = vec![
PaData {
padata_type: PA_ENC_TIMESTAMP,
padata_value: pa_enc_ts_value,
},
PaData {
padata_type: PA_PAC_REQUEST,
padata_value: pa_pac_value,
},
];
let as_req = encode_as_req(&cname, realm, &sname, nonce, &etypes, &padata);
send_to_kdc(kdc_config, &as_req).await?
} else {
return Err(Error::Auth {
message: format!(
"Kerberos AS exchange failed: KRB-ERROR code {} ({})",
krb_error.error_code,
krb_error.e_text.unwrap_or_default()
),
});
}
} else {
response
};
if !response.is_empty() && response[0] == 0x7e {
let krb_error = parse_krb_error(&response)?;
return Err(Error::Auth {
message: format!(
"Kerberos AS exchange failed: KRB-ERROR code {} ({})",
krb_error.error_code,
krb_error.e_text.unwrap_or_default()
),
});
}
let as_rep = parse_kdc_rep(&response)?;
if as_rep.msg_type != 11 {
return Err(Error::invalid_data(format!(
"Kerberos: expected AS-REP (msg_type 11), got {}",
as_rep.msg_type
)));
}
self.etype = etype_from_i32(as_rep.enc_part.etype)?;
debug!(
"kerberos: AS-REP etype={}, kvno={:?}, cipher_len={}, crealm={}, cname={:?}",
as_rep.enc_part.etype,
as_rep.enc_part.kvno,
as_rep.enc_part.cipher.len(),
as_rep.crealm,
as_rep.cname.name_string,
);
let user_key = self.derive_user_key();
debug!(
"kerberos: user_key len={}, etype={:?}, salt={}{}, key_prefix={:02x?}",
user_key.len(),
self.etype,
&self.credentials.realm,
&self.credentials.username,
&user_key[..user_key.len().min(8)],
);
let enc_part_plain = kerberos_decrypt(
&user_key,
KEY_USAGE_AS_REP_ENC_PART,
&as_rep.enc_part.cipher,
self.etype,
)?;
let enc_kdc_rep = parse_enc_kdc_rep_part(&enc_part_plain)?;
trace!(
"kerberos: AS session key type={}, len={}",
enc_kdc_rep.key.keytype,
enc_kdc_rep.key.keyvalue.len()
);
self.tgt = Some(as_rep.ticket);
self.as_session_key = Some(enc_kdc_rep.key.keyvalue);
Ok(())
}
async fn tgs_exchange(&mut self, kdc_config: &KdcConfig, server_hostname: &str) -> Result<()> {
let tgt = self
.tgt
.as_ref()
.ok_or_else(|| Error::Auth {
message: "TGS exchange requires a TGT (run AS exchange first)".to_string(),
})?
.clone();
let as_session_key = self
.as_session_key
.as_ref()
.ok_or_else(|| Error::Auth {
message: "TGS exchange requires AS session key".to_string(),
})?
.clone();
let realm = &self.credentials.realm;
let username = &self.credentials.username;
let sname = PrincipalName {
name_type: 2, name_string: vec!["cifs".to_string(), server_hostname.to_string()],
};
let cname = PrincipalName {
name_type: 1,
name_string: vec![username.clone()],
};
let nonce = generate_nonce();
let etypes = [
EncryptionType::Aes256CtsHmacSha196,
EncryptionType::Aes128CtsHmacSha196,
EncryptionType::Rc4Hmac,
];
let req_body = encode_tgs_req_body(realm, &sname, nonce, &etypes);
let body_checksum = compute_checksum(&as_session_key, 6, &req_body, self.etype);
let checksum_type: i32 = match self.etype {
EncryptionType::Aes256CtsHmacSha196 => 16, EncryptionType::Aes128CtsHmacSha196 => 15, EncryptionType::Rc4Hmac => -138, };
let (ctime, cusec) = current_kerberos_time();
let authenticator_plain = encode_authenticator(
realm,
&cname,
&ctime,
cusec,
None,
None,
Some((&body_checksum, checksum_type)),
);
debug!(
"kerberos: TGS authenticator plain ({} bytes), session key prefix={:02x?}",
authenticator_plain.len(),
&as_session_key[..as_session_key.len().min(8)]
);
let encrypted_authenticator = kerberos_encrypt(
&as_session_key,
KEY_USAGE_AP_REQ_AUTHENTICATOR,
&authenticator_plain,
self.etype,
);
let authenticator_enc_data = EncryptedData {
etype: self.etype as i32,
kvno: None,
cipher: encrypted_authenticator,
};
let tgt_ap_req = encode_ap_req(&tgt, &authenticator_enc_data, false);
let tgs_req = encode_tgs_req(realm, &sname, nonce, &etypes, &tgt_ap_req);
let response = send_to_kdc(kdc_config, &tgs_req).await?;
if !response.is_empty() && response[0] == 0x7e {
let krb_error = parse_krb_error(&response)?;
return Err(Error::Auth {
message: format!(
"Kerberos TGS exchange failed: KRB-ERROR code {} ({})",
krb_error.error_code,
krb_error.e_text.unwrap_or_default()
),
});
}
let tgs_rep = parse_kdc_rep(&response)?;
debug!(
"kerberos: TGS-REP ticket etype={}, kvno={:?}, cipher_len={}",
tgs_rep.ticket.enc_part.etype,
tgs_rep.ticket.enc_part.kvno,
tgs_rep.ticket.enc_part.cipher.len()
);
debug!(
"kerberos: TGS-REP enc-part etype={}, kvno={:?}",
tgs_rep.enc_part.etype, tgs_rep.enc_part.kvno
);
if tgs_rep.msg_type != 13 {
return Err(Error::invalid_data(format!(
"Kerberos: expected TGS-REP (msg_type 13), got {}",
tgs_rep.msg_type
)));
}
let enc_part_plain = match kerberos_decrypt(
&as_session_key,
KEY_USAGE_TGS_REP_ENC_PART_SESSION_KEY,
&tgs_rep.enc_part.cipher,
self.etype,
) {
Ok(plain) => plain,
Err(_) => {
debug!("kerberos: TGS-REP decryption with key usage 8 failed, trying 9");
kerberos_decrypt(
&as_session_key,
KEY_USAGE_TGS_REP_ENC_PART_SUBKEY,
&tgs_rep.enc_part.cipher,
self.etype,
)?
}
};
let enc_kdc_rep = parse_enc_kdc_rep_part(&enc_part_plain)?;
trace!(
"kerberos: TGS session key type={}, len={}",
enc_kdc_rep.key.keytype,
enc_kdc_rep.key.keyvalue.len()
);
debug!(
"kerberos: service ticket has raw_bytes={}, raw_len={:?}",
tgs_rep.ticket.raw_bytes.is_some(),
tgs_rep.ticket.raw_bytes.as_ref().map(|b| b.len())
);
let tgs_key_etype = match enc_kdc_rep.key.keytype {
18 => EncryptionType::Aes256CtsHmacSha196,
17 => EncryptionType::Aes128CtsHmacSha196,
23 => EncryptionType::Rc4Hmac,
other => {
return Err(Error::Auth {
message: format!("TGS session key has unsupported etype {other}"),
});
}
};
self.etype = tgs_key_etype;
self.service_ticket = Some(tgs_rep.ticket);
self.tgs_session_key = Some(enc_kdc_rep.key.keyvalue.clone());
self.session_key = Some(enc_kdc_rep.key.keyvalue);
Ok(())
}
fn build_ap_req(&mut self) -> Result<()> {
let service_ticket = self
.service_ticket
.as_ref()
.ok_or_else(|| Error::Auth {
message: "AP-REQ requires a service ticket (run TGS exchange first)".to_string(),
})?
.clone();
let tgs_session_key = self
.tgs_session_key
.as_ref()
.ok_or_else(|| Error::Auth {
message: "AP-REQ requires TGS session key".to_string(),
})?
.clone();
let realm = &self.credentials.realm;
let username = &self.credentials.username;
let cname = PrincipalName {
name_type: 1,
name_string: vec![username.clone()],
};
let (ctime, cusec) = current_kerberos_time();
let authenticator_plain = encode_authenticator(
realm, &cname, &ctime, cusec, None, None, None, );
let encrypted_authenticator = kerberos_encrypt(
&tgs_session_key,
KEY_USAGE_AP_REQ_AUTHENTICATOR_SPNEGO,
&authenticator_plain,
self.etype,
);
let authenticator_enc_data = EncryptedData {
etype: self.etype as i32,
kvno: None,
cipher: encrypted_authenticator,
};
let ap_req = encode_ap_req(&service_ticket, &authenticator_enc_data, true);
let gss_mech_token = {
let oid_bytes: &[u8] = &OID_KERBEROS[2..]; let mut inner = Vec::new();
inner.push(0x06); inner.push(oid_bytes.len() as u8);
inner.extend_from_slice(oid_bytes);
inner.extend_from_slice(&[0x01, 0x00]); inner.extend_from_slice(&ap_req);
let mut token = Vec::new();
token.push(0x60); if inner.len() < 128 {
token.push(inner.len() as u8);
} else if inner.len() < 256 {
token.push(0x81);
token.push(inner.len() as u8);
} else {
token.push(0x82);
token.push((inner.len() >> 8) as u8);
token.push((inner.len() & 0xff) as u8);
}
token.extend_from_slice(&inner);
token
};
let spnego_token = wrap_neg_token_init(&[OID_MS_KERBEROS], &gss_mech_token);
self.session_key = Some(tgs_session_key);
self.ap_req_bytes = Some(spnego_token);
Ok(())
}
pub fn process_mutual_auth_token(&mut self, token_bytes: &[u8]) -> Result<()> {
use crate::auth::kerberos::messages::{
parse_ap_rep, parse_enc_ap_rep_part, parse_krb_error,
};
let inner = if !token_bytes.is_empty() && token_bytes[0] == 0x60 {
let (_, gss_inner, _) =
crate::auth::kerberos::messages::parse_gss_api_wrapper(token_bytes)?;
gss_inner
} else {
token_bytes.to_vec()
};
if inner.len() < 2 {
return Err(Error::invalid_data("Kerberos: mutual auth token too short"));
}
let token_id = [inner[0], inner[1]];
let krb_data = &inner[2..];
match token_id {
[0x02, 0x00] => {
debug!("kerberos: processing AP-REP from server");
let ap_rep = parse_ap_rep(krb_data)?;
const KEY_USAGE_AP_REP_ENC_PART: u32 = 12;
let current_key = self.session_key.as_ref().ok_or_else(|| Error::Auth {
message: "No session key available to decrypt AP-REP".to_string(),
})?;
let etype = match ap_rep.enc_part.etype {
18 => EncryptionType::Aes256CtsHmacSha196,
17 => EncryptionType::Aes128CtsHmacSha196,
23 => EncryptionType::Rc4Hmac,
other => {
return Err(Error::Auth {
message: format!("AP-REP: unsupported etype {other}"),
})
}
};
let plain = kerberos_decrypt(
current_key,
KEY_USAGE_AP_REP_ENC_PART,
&ap_rep.enc_part.cipher,
etype,
)?;
let enc_part = parse_enc_ap_rep_part(&plain)?;
if let Some(server_subkey) = enc_part.subkey {
debug!(
"kerberos: AP-REP server subkey, etype={}, len={}",
server_subkey.keytype,
server_subkey.keyvalue.len()
);
self.session_key = Some(server_subkey.keyvalue);
} else {
debug!("kerberos: AP-REP has no server subkey");
}
}
[0x03, 0x00] => {
match parse_krb_error(krb_data) {
Ok(err) => {
debug!(
"kerberos: mutual auth KRB-ERROR code={}, realm={}, sname={:?}, e_text={:?}, e_data={:02x?}",
err.error_code, err.realm, err.sname, err.e_text,
err.e_data.as_deref().unwrap_or(&[])
);
}
Err(e) => {
debug!("kerberos: failed to parse KRB-ERROR in mutual auth: {}", e);
}
}
}
_ => {
debug!(
"kerberos: unexpected mutual auth token ID: {:02x} {:02x}",
token_id[0], token_id[1]
);
}
}
Ok(())
}
fn derive_user_key(&self) -> Vec<u8> {
let salt = format!("{}{}", self.credentials.realm, self.credentials.username);
match self.etype {
EncryptionType::Aes256CtsHmacSha196 => {
string_to_key_aes(&self.credentials.password, &salt, 32)
}
EncryptionType::Aes128CtsHmacSha196 => {
string_to_key_aes(&self.credentials.password, &salt, 16)
}
EncryptionType::Rc4Hmac => string_to_key_rc4(&self.credentials.password),
}
}
fn extract_best_etype(&self, e_data: &[u8]) -> Option<EncryptionType> {
let entries = parse_method_data(e_data).ok()?;
for entry in &entries {
if entry.padata_type == PA_ETYPE_INFO2 {
if let Some(etype) = parse_etype_info2_best(&entry.padata_value) {
return Some(etype);
}
}
}
None
}
}
fn encode_encrypted_data_raw(ed: &EncryptedData) -> Vec<u8> {
let etype = der_context(0, &der_integer(ed.etype));
let cipher = der_context(2, &der_octet_string(&ed.cipher));
if let Some(kvno) = ed.kvno {
let kvno_enc = der_context(1, &der_integer(kvno));
der_sequence(&[&etype, &kvno_enc, &cipher])
} else {
der_sequence(&[&etype, &cipher])
}
}
fn encode_pa_pac_request(include_pac: bool) -> Vec<u8> {
let bool_val: &[u8] = if include_pac {
&[0x01, 0x01, 0xff]
} else {
&[0x01, 0x01, 0x00]
};
let include = der_context(0, bool_val);
der_sequence(&[&include])
}
fn parse_method_data(data: &[u8]) -> Result<Vec<PaData>> {
let (tag, seq_data, _) = parse_der_tlv_local(data)?;
if tag != 0x30 {
return Err(Error::invalid_data(format!(
"Kerberos: expected SEQUENCE for METHOD-DATA, got 0x{tag:02x}"
)));
}
let mut entries = Vec::new();
let mut pos = 0;
while pos < seq_data.len() {
let (entry_tag, entry_data, consumed) = parse_der_tlv_local(&seq_data[pos..])?;
if entry_tag == 0x30 {
let fields = parse_sequence_fields_local(entry_data)?;
let mut padata_type = None;
let mut padata_value = None;
for (ftag, fvalue) in &fields {
match ftag {
0xa1 => padata_type = Some(parse_der_integer_local(fvalue)?),
0xa2 => padata_value = Some(parse_der_octet_string_local(fvalue)?),
_ => {}
}
}
if let (Some(pt), Some(pv)) = (padata_type, padata_value) {
entries.push(PaData {
padata_type: pt,
padata_value: pv,
});
}
}
pos += consumed;
}
Ok(entries)
}
fn parse_etype_info2_best(data: &[u8]) -> Option<EncryptionType> {
let (tag, seq_data, _) = parse_der_tlv_local(data).ok()?;
if tag != 0x30 {
return None;
}
let mut best: Option<EncryptionType> = None;
let mut pos = 0;
while pos < seq_data.len() {
let (entry_tag, entry_data, consumed) = parse_der_tlv_local(&seq_data[pos..]).ok()?;
if entry_tag == 0x30 {
let fields = parse_sequence_fields_local(entry_data).ok()?;
for (ftag, fvalue) in &fields {
if *ftag == 0xa0 {
if let Ok(etype_val) = parse_der_integer_local(fvalue) {
if let Ok(et) = etype_from_i32(etype_val) {
match (&best, et) {
(None, _) => best = Some(et),
(Some(EncryptionType::Rc4Hmac), _)
if et != EncryptionType::Rc4Hmac =>
{
best = Some(et);
}
(
Some(EncryptionType::Aes128CtsHmacSha196),
EncryptionType::Aes256CtsHmacSha196,
) => {
best = Some(et);
}
_ => {}
}
}
}
}
}
}
pos += consumed;
}
best
}
fn parse_der_tlv_local(data: &[u8]) -> Result<(u8, &[u8], usize)> {
if data.is_empty() {
return Err(Error::invalid_data("Kerberos: truncated DER TLV"));
}
let tag = data[0];
let (len, len_bytes) = parse_der_length_local(&data[1..])?;
let header_len = 1 + len_bytes;
let total = header_len + len;
if data.len() < total {
return Err(Error::invalid_data(format!(
"Kerberos: DER TLV truncated: need {total} bytes, have {}",
data.len()
)));
}
Ok((tag, &data[header_len..total], total))
}
fn parse_der_length_local(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(Error::invalid_data("Kerberos: truncated DER length"));
}
let first = data[0];
if first < 128 {
Ok((first as usize, 1))
} else if first == 0x81 {
if data.len() < 2 {
return Err(Error::invalid_data("Kerberos: truncated DER length (0x81)"));
}
Ok((data[1] as usize, 2))
} else if first == 0x82 {
if data.len() < 3 {
return Err(Error::invalid_data("Kerberos: truncated DER length (0x82)"));
}
let len = ((data[1] as usize) << 8) | (data[2] as usize);
Ok((len, 3))
} else {
Err(Error::invalid_data(format!(
"Kerberos: unsupported DER length encoding: 0x{first:02x}"
)))
}
}
fn parse_sequence_fields_local(data: &[u8]) -> Result<Vec<(u8, Vec<u8>)>> {
let mut fields = Vec::new();
let mut pos = 0;
while pos < data.len() {
let (tag, value, consumed) = parse_der_tlv_local(&data[pos..])?;
fields.push((tag, value.to_vec()));
pos += consumed;
}
Ok(fields)
}
fn parse_der_integer_local(data: &[u8]) -> Result<i32> {
let (tag, value, _) = parse_der_tlv_local(data)?;
if tag != 0x02 {
return Err(Error::invalid_data(format!(
"Kerberos: expected INTEGER (0x02), got 0x{tag:02x}"
)));
}
if value.is_empty() {
return Err(Error::invalid_data("Kerberos: empty INTEGER"));
}
let negative = value[0] & 0x80 != 0;
let mut val: i64 = if negative { -1 } else { 0 };
for &b in value {
val = (val << 8) | (b as i64);
}
Ok(val as i32)
}
fn parse_der_octet_string_local(data: &[u8]) -> Result<Vec<u8>> {
let (tag, value, _) = parse_der_tlv_local(data)?;
if tag != 0x04 {
return Err(Error::invalid_data(format!(
"Kerberos: expected OCTET STRING (0x04), got 0x{tag:02x}"
)));
}
Ok(value.to_vec())
}
fn der_length(len: usize) -> Vec<u8> {
if len < 128 {
vec![len as u8]
} else if len < 256 {
vec![0x81, len as u8]
} else {
vec![0x82, (len >> 8) as u8, (len & 0xff) as u8]
}
}
fn der_tlv(tag: u8, data: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
out.extend_from_slice(&der_length(data.len()));
out.extend_from_slice(data);
out
}
fn der_context(tag_num: u8, data: &[u8]) -> Vec<u8> {
der_tlv(0xa0 | tag_num, data)
}
fn der_integer(val: i32) -> Vec<u8> {
let bytes = val.to_be_bytes();
let mut start = 0;
if val >= 0 {
while start < 3 && bytes[start] == 0x00 && bytes[start + 1] & 0x80 == 0 {
start += 1;
}
} else {
while start < 3 && bytes[start] == 0xff && bytes[start + 1] & 0x80 != 0 {
start += 1;
}
}
der_tlv(0x02, &bytes[start..])
}
fn der_octet_string(data: &[u8]) -> Vec<u8> {
der_tlv(0x04, data)
}
fn der_sequence(items: &[&[u8]]) -> Vec<u8> {
let mut contents = Vec::new();
for item in items {
contents.extend_from_slice(item);
}
der_tlv(0x30, &contents)
}
fn current_kerberos_time() -> (String, u32) {
use std::time::SystemTime;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system clock before epoch");
let total_secs = now.as_secs();
let usec = now.subsec_micros();
let (year, month, day, hour, minute, second) = secs_to_datetime(total_secs);
let time_str = format!(
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
year, month, day, hour, minute, second
);
(time_str, usec)
}
fn secs_to_datetime(secs: u64) -> (u32, u32, u32, u32, u32, u32) {
let days = secs / 86400;
let time_of_day = secs % 86400;
let hour = (time_of_day / 3600) as u32;
let minute = ((time_of_day % 3600) / 60) as u32;
let second = (time_of_day % 60) as u32;
let z = days as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = (yoe as i64) + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = if m <= 2 { y + 1 } else { y };
(y as u32, m as u32, d as u32, hour, minute, second)
}
fn etype_from_code(code: i32) -> Result<EncryptionType> {
match code {
18 => Ok(EncryptionType::Aes256CtsHmacSha196),
17 => Ok(EncryptionType::Aes128CtsHmacSha196),
23 => Ok(EncryptionType::Rc4Hmac),
other => Err(Error::Auth {
message: format!("unsupported etype {other}"),
}),
}
}
fn generate_nonce() -> u32 {
let mut buf = [0u8; 4];
getrandom::fill(&mut buf).expect("CSPRNG failed");
u32::from_ne_bytes(buf) & 0x7FFF_FFFF }
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::kerberos::crypto::{
generate_random_key, kerberos_decrypt, kerberos_encrypt, string_to_key_aes,
};
use crate::auth::kerberos::messages::{
encode_ap_req, encode_as_req, encode_authenticator, encode_pa_enc_timestamp, EncryptedData,
PrincipalName, Ticket,
};
use crate::auth::spnego::OID_NTLMSSP;
#[test]
fn secs_to_datetime_epoch() {
let (y, m, d, h, mi, s) = secs_to_datetime(0);
assert_eq!((y, m, d, h, mi, s), (1970, 1, 1, 0, 0, 0));
}
#[test]
fn secs_to_datetime_known_date() {
let (y, m, d, h, mi, s) = secs_to_datetime(1775649600);
assert_eq!((y, m, d, h, mi, s), (2026, 4, 8, 12, 0, 0));
}
#[test]
fn secs_to_datetime_leap_year() {
let (y, m, d, _, _, _) = secs_to_datetime(1709164800);
assert_eq!((y, m, d), (2024, 2, 29));
}
#[test]
fn current_kerberos_time_format() {
let (time_str, _cusec) = current_kerberos_time();
assert_eq!(
time_str.len(),
15,
"GeneralizedTime should be 15 chars: {time_str}"
);
assert!(time_str.ends_with('Z'), "should end with Z: {time_str}");
assert!(time_str[..4].parse::<u32>().is_ok(), "year: {time_str}");
}
#[test]
fn generate_nonce_is_positive() {
for _ in 0..100 {
let n = generate_nonce();
assert!(n <= 0x7FFF_FFFF, "nonce should be positive: {n}");
}
}
#[test]
fn generate_nonce_not_constant() {
let n1 = generate_nonce();
let n2 = generate_nonce();
if n1 == n2 {
let n3 = generate_nonce();
assert!(
n1 != n3 || n2 != n3,
"three consecutive identical nonces is suspicious"
);
}
}
#[test]
fn encode_pa_pac_request_true() {
let encoded = encode_pa_pac_request(true);
assert_eq!(encoded[0], 0x30); assert!(encoded.windows(3).any(|w| w == [0x01, 0x01, 0xff]));
}
#[test]
fn encode_pa_pac_request_false() {
let encoded = encode_pa_pac_request(false);
assert_eq!(encoded[0], 0x30);
assert!(encoded.windows(3).any(|w| w == [0x01, 0x01, 0x00]));
}
#[test]
fn pa_enc_timestamp_produces_valid_encrypted_data() {
let key = string_to_key_aes("password", "EXAMPLE.COMuser", 32);
let timestamp_plain = encode_pa_enc_timestamp("20260408120000Z", 123456);
let ciphertext = kerberos_encrypt(
&key,
KEY_USAGE_PA_ENC_TIMESTAMP,
×tamp_plain,
EncryptionType::Aes256CtsHmacSha196,
);
assert!(
ciphertext.len() > 12,
"ciphertext too short: {}",
ciphertext.len()
);
let decrypted = kerberos_decrypt(
&key,
KEY_USAGE_PA_ENC_TIMESTAMP,
&ciphertext,
EncryptionType::Aes256CtsHmacSha196,
)
.unwrap();
assert_eq!(decrypted, timestamp_plain);
}
#[test]
fn authenticator_encrypt_decrypt_roundtrip() {
let key = generate_random_key(EncryptionType::Aes256CtsHmacSha196);
let cname = PrincipalName {
name_type: 1,
name_string: vec!["user".to_string()],
};
let authenticator_plain = encode_authenticator(
"EXAMPLE.COM",
&cname,
"20260408120000Z",
0,
None,
None,
None,
);
let encrypted = kerberos_encrypt(
&key,
KEY_USAGE_AP_REQ_AUTHENTICATOR,
&authenticator_plain,
EncryptionType::Aes256CtsHmacSha196,
);
let decrypted = kerberos_decrypt(
&key,
KEY_USAGE_AP_REQ_AUTHENTICATOR,
&encrypted,
EncryptionType::Aes256CtsHmacSha196,
)
.unwrap();
assert_eq!(decrypted, authenticator_plain);
}
#[test]
fn build_ap_req_produces_spnego_wrapped_token() {
let ticket = Ticket {
tkt_vno: 5,
realm: "EXAMPLE.COM".to_string(),
sname: PrincipalName {
name_type: 2,
name_string: vec!["cifs".to_string(), "server.example.com".to_string()],
},
enc_part: EncryptedData {
etype: 18,
kvno: Some(1),
cipher: vec![0xDE, 0xAD, 0xBE, 0xEF],
},
raw_bytes: None,
};
let session_key = generate_random_key(EncryptionType::Aes256CtsHmacSha196);
let cname = PrincipalName {
name_type: 1,
name_string: vec!["user".to_string()],
};
let authenticator_plain = encode_authenticator(
"EXAMPLE.COM",
&cname,
"20260408120000Z",
0,
None,
None,
None,
);
let encrypted_auth = kerberos_encrypt(
&session_key,
KEY_USAGE_AP_REQ_AUTHENTICATOR,
&authenticator_plain,
EncryptionType::Aes256CtsHmacSha196,
);
let auth_enc_data = EncryptedData {
etype: 18,
kvno: None,
cipher: encrypted_auth,
};
let ap_req = encode_ap_req(&ticket, &auth_enc_data, false);
assert_eq!(ap_req[0], 0x6e, "AP-REQ should start with APPLICATION [14]");
let spnego = wrap_neg_token_init(&[OID_KERBEROS, OID_NTLMSSP], &ap_req);
assert_eq!(
spnego[0], 0x60,
"SPNEGO token should start with APPLICATION [0]"
);
assert!(
spnego
.windows(OID_KERBEROS.len())
.any(|w| w == OID_KERBEROS),
"SPNEGO token should contain the Kerberos OID"
);
}
#[test]
fn as_req_with_padata_contains_pa_types() {
let cname = PrincipalName {
name_type: 1,
name_string: vec!["user".to_string()],
};
let sname = PrincipalName {
name_type: 2,
name_string: vec!["krbtgt".to_string(), "EXAMPLE.COM".to_string()],
};
let pa_pac = PaData {
padata_type: PA_PAC_REQUEST,
padata_value: encode_pa_pac_request(true),
};
let encoded = encode_as_req(
&cname,
"EXAMPLE.COM",
&sname,
12345,
&[EncryptionType::Aes256CtsHmacSha196],
&[pa_pac],
);
assert_eq!(encoded[0], 0x6a);
assert!(
encoded.len() > 50,
"AS-REQ with padata should be substantial"
);
}
#[test]
fn encode_encrypted_data_raw_has_sequence_tag() {
let ed = EncryptedData {
etype: 18,
kvno: None,
cipher: vec![0x01, 0x02, 0x03],
};
let encoded = encode_encrypted_data_raw(&ed);
assert_eq!(encoded[0], 0x30, "EncryptedData should be a SEQUENCE");
}
#[test]
fn encode_encrypted_data_raw_with_kvno() {
let ed = EncryptedData {
etype: 18,
kvno: Some(2),
cipher: vec![0x01, 0x02, 0x03],
};
let encoded = encode_encrypted_data_raw(&ed);
assert!(
encoded.windows(2).any(|w| w[0] == 0xa1),
"should contain kvno field [1]"
);
}
#[test]
fn parse_etype_info2_best_aes256() {
let entry_18 = der_sequence(&[&der_context(0, &der_integer(18))]);
let entry_23 = der_sequence(&[&der_context(0, &der_integer(23))]);
let etype_info2 = der_sequence(&[&entry_18, &entry_23]);
let best = parse_etype_info2_best(&etype_info2);
assert_eq!(best, Some(EncryptionType::Aes256CtsHmacSha196));
}
#[test]
fn parse_etype_info2_best_prefers_aes256_over_aes128() {
let entry_17 = der_sequence(&[&der_context(0, &der_integer(17))]);
let entry_18 = der_sequence(&[&der_context(0, &der_integer(18))]);
let etype_info2 = der_sequence(&[&entry_17, &entry_18]);
let best = parse_etype_info2_best(&etype_info2);
assert_eq!(best, Some(EncryptionType::Aes256CtsHmacSha196));
}
#[test]
fn parse_etype_info2_best_rc4_only() {
let entry_23 = der_sequence(&[&der_context(0, &der_integer(23))]);
let etype_info2 = der_sequence(&[&entry_23]);
let best = parse_etype_info2_best(&etype_info2);
assert_eq!(best, Some(EncryptionType::Rc4Hmac));
}
#[test]
fn parse_etype_info2_best_unknown_only() {
let entry_99 = der_sequence(&[&der_context(0, &der_integer(99))]);
let etype_info2 = der_sequence(&[&entry_99]);
let best = parse_etype_info2_best(&etype_info2);
assert_eq!(best, None);
}
#[test]
fn parse_method_data_extracts_padata() {
let pa_value = vec![0x01, 0x02, 0x03];
let pa_type_enc = der_context(1, &der_integer(PA_ETYPE_INFO2));
let pa_value_enc = der_context(2, &der_octet_string(&pa_value));
let pa_data = der_sequence(&[&pa_type_enc, &pa_value_enc]);
let method_data = der_sequence(&[&pa_data]);
let entries = parse_method_data(&method_data).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].padata_type, PA_ETYPE_INFO2);
assert_eq!(entries[0].padata_value, pa_value);
}
#[test]
fn authenticator_initial_state() {
let auth = KerberosAuthenticator::new(KerberosCredentials {
username: "user".to_string(),
password: "pass".to_string(),
realm: "EXAMPLE.COM".to_string(),
kdc_address: "kdc.example.com".to_string(),
});
assert!(auth.token().is_none());
assert!(auth.session_key().is_none());
assert!(auth.tgt.is_none());
assert!(auth.as_session_key.is_none());
assert!(auth.service_ticket.is_none());
assert!(auth.tgs_session_key.is_none());
}
#[test]
fn derive_user_key_aes256() {
let auth = KerberosAuthenticator {
credentials: KerberosCredentials {
username: "user".to_string(),
password: "password".to_string(),
realm: "EXAMPLE.COM".to_string(),
kdc_address: "kdc.example.com".to_string(),
},
tgt: None,
as_session_key: None,
service_ticket: None,
tgs_session_key: None,
ap_req_bytes: None,
session_key: None,
etype: EncryptionType::Aes256CtsHmacSha196,
};
let key = auth.derive_user_key();
assert_eq!(key.len(), 32, "AES-256 key should be 32 bytes");
let expected = string_to_key_aes("password", "EXAMPLE.COMuser", 32);
assert_eq!(key, expected);
}
#[test]
fn derive_user_key_aes128() {
let mut auth = KerberosAuthenticator::new(KerberosCredentials {
username: "user".to_string(),
password: "password".to_string(),
realm: "EXAMPLE.COM".to_string(),
kdc_address: "kdc.example.com".to_string(),
});
auth.etype = EncryptionType::Aes128CtsHmacSha196;
let key = auth.derive_user_key();
assert_eq!(key.len(), 16, "AES-128 key should be 16 bytes");
}
#[test]
fn derive_user_key_rc4() {
let mut auth = KerberosAuthenticator::new(KerberosCredentials {
username: "user".to_string(),
password: "password".to_string(),
realm: "EXAMPLE.COM".to_string(),
kdc_address: "kdc.example.com".to_string(),
});
auth.etype = EncryptionType::Rc4Hmac;
let key = auth.derive_user_key();
assert_eq!(key.len(), 16, "RC4 key (NT hash) should be 16 bytes");
}
}