use crate::errors::{AuthError, Result};
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
use base64::Engine as _;
use ring::rand::SecureRandom;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use subtle::ConstantTimeEq;
use tokio::sync::RwLock;
const ETYPE_AES128: i32 = 17;
const ETYPE_AES256: i32 = 18;
const KEY_USAGE_TICKET: u32 = 2;
const KEY_USAGE_AP_REQ_AUTH: u32 = 11;
const AES_BLOCK: usize = 16;
const HMAC_LEN: usize = 12;
const CONFOUNDER_LEN: usize = 16;
const SPNEGO_OID_BYTES: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
const KRB5_OID_BYTES: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02];
#[allow(dead_code)]
#[derive(Debug)]
struct DerTlv<'a> {
class: u8,
constructed: bool,
tag_num: u32,
value: &'a [u8],
}
fn parse_der(data: &[u8]) -> Result<(DerTlv<'_>, &[u8])> {
if data.is_empty() {
return Err(AuthError::validation("Empty DER data"));
}
let b0 = data[0];
let class = b0 >> 6;
let constructed = (b0 & 0x20) != 0;
let mut pos: usize = 1;
let tag_num = if (b0 & 0x1f) == 0x1f {
let mut t: u32 = 0;
loop {
if pos >= data.len() {
return Err(AuthError::validation("DER tag truncated"));
}
let b = data[pos];
pos += 1;
t = t
.checked_shl(7)
.ok_or_else(|| AuthError::validation("DER tag too large"))?
| (b & 0x7f) as u32;
if (b & 0x80) == 0 {
break;
}
}
t
} else {
(b0 & 0x1f) as u32
};
if pos >= data.len() {
return Err(AuthError::validation("DER length missing"));
}
let len_byte = data[pos];
pos += 1;
let length = if len_byte < 0x80 {
len_byte as usize
} else if len_byte == 0x80 {
return Err(AuthError::validation(
"Indefinite length not supported in DER",
));
} else {
let num_bytes = (len_byte & 0x7f) as usize;
if num_bytes > 4 || pos + num_bytes > data.len() {
return Err(AuthError::validation("DER length overflow"));
}
let mut l: usize = 0;
for &b in &data[pos..pos + num_bytes] {
l = l
.checked_shl(8)
.ok_or_else(|| AuthError::validation("DER length too large"))?
| b as usize;
}
pos += num_bytes;
l
};
if pos + length > data.len() {
return Err(AuthError::validation("DER value truncated"));
}
Ok((
DerTlv {
class,
constructed,
tag_num,
value: &data[pos..pos + length],
},
&data[pos + length..],
))
}
fn parse_der_contents(data: &[u8]) -> Result<Vec<DerTlv<'_>>> {
let mut result = Vec::new();
let mut remaining = data;
while !remaining.is_empty() {
let (tlv, rest) = parse_der(remaining)?;
result.push(tlv);
remaining = rest;
}
Ok(result)
}
fn get_ctx_field<'a, 'b>(fields: &'b [DerTlv<'a>], tag: u32) -> Option<&'b DerTlv<'a>> {
fields.iter().find(|f| f.class == 2 && f.tag_num == tag)
}
fn unwrap_explicit<'a>(tlv: &DerTlv<'a>) -> Result<DerTlv<'a>> {
let (inner, _) = parse_der(tlv.value)?;
Ok(inner)
}
fn parse_der_integer(data: &[u8]) -> Result<i64> {
if data.is_empty() {
return Err(AuthError::validation("Empty INTEGER"));
}
let mut val: i64 = if data[0] & 0x80 != 0 { -1 } else { 0 };
for &b in data {
val = (val << 8) | b as i64;
}
Ok(val)
}
fn parse_der_string(data: &[u8]) -> Result<String> {
String::from_utf8(data.to_vec())
.map_err(|e| AuthError::validation(format!("Invalid string encoding: {e}")))
}
fn parse_kerberos_time(data: &[u8]) -> Result<u64> {
let s = parse_der_string(data)?;
if s.len() < 15 || !s.ends_with('Z') {
return Err(AuthError::validation("Invalid KerberosTime format"));
}
let dt = chrono::NaiveDateTime::parse_from_str(&s[..14], "%Y%m%d%H%M%S")
.map_err(|e| AuthError::validation(format!("Invalid KerberosTime: {e}")))?;
Ok(dt.and_utc().timestamp() as u64)
}
fn test_bit_flag(flags_data: &[u8], bit_num: usize) -> bool {
if flags_data.is_empty() {
return false;
}
let byte_idx = 1 + bit_num / 8;
let bit_idx = 7 - (bit_num % 8);
if byte_idx >= flags_data.len() {
return false;
}
(flags_data[byte_idx] >> bit_idx) & 1 == 1
}
fn oid_matches(tlv: &DerTlv<'_>, expected: &[u8]) -> bool {
tlv.class == 0 && tlv.tag_num == 6 && tlv.value == expected
}
struct ParsedEncryptedData<'a> {
etype: i32,
kvno: Option<u32>,
cipher: &'a [u8],
}
struct ParsedPrincipalName {
components: Vec<String>,
}
impl ParsedPrincipalName {
fn to_string_without_realm(&self) -> String {
self.components.join("/")
}
}
struct ParsedTicket<'a> {
realm: String,
sname: ParsedPrincipalName,
enc_part: ParsedEncryptedData<'a>,
}
#[allow(dead_code)]
struct ParsedApReq<'a> {
ap_options: u32,
ticket: ParsedTicket<'a>,
authenticator: ParsedEncryptedData<'a>,
}
#[allow(dead_code)]
struct DecryptedTicketPart {
flags_raw: Vec<u8>,
session_key_type: i32,
session_key_value: Vec<u8>,
crealm: String,
cname: ParsedPrincipalName,
auth_time: u64,
end_time: u64,
start_time: Option<u64>,
renew_till: Option<u64>,
}
struct DecryptedAuthenticator {
crealm: String,
cname: ParsedPrincipalName,
cusec: u32,
ctime: u64,
}
fn parse_encrypted_data<'a>(data: &'a [u8]) -> Result<ParsedEncryptedData<'a>> {
let (seq, _) = parse_der(data)?;
if seq.tag_num != 16 {
return Err(AuthError::validation("EncryptedData: expected SEQUENCE"));
}
let fields = parse_der_contents(seq.value)?;
let etype_f = get_ctx_field(&fields, 0)
.ok_or_else(|| AuthError::validation("EncryptedData missing etype"))?;
let etype = parse_der_integer(unwrap_explicit(etype_f)?.value)? as i32;
let kvno = if let Some(f) = get_ctx_field(&fields, 1) {
Some(parse_der_integer(unwrap_explicit(f)?.value)? as u32)
} else {
None
};
let cipher_f = get_ctx_field(&fields, 2)
.ok_or_else(|| AuthError::validation("EncryptedData missing cipher"))?;
let cipher_tlv = unwrap_explicit(cipher_f)?;
Ok(ParsedEncryptedData {
etype,
kvno,
cipher: cipher_tlv.value,
})
}
fn parse_principal_name(data: &[u8]) -> Result<ParsedPrincipalName> {
let (seq, _) = parse_der(data)?;
let fields = parse_der_contents(seq.value)?;
let strings_f = get_ctx_field(&fields, 1)
.ok_or_else(|| AuthError::validation("PrincipalName missing name-string"))?;
let (strings_seq, _) = parse_der(strings_f.value)?;
let string_tlvs = parse_der_contents(strings_seq.value)?;
let mut components = Vec::new();
for tlv in &string_tlvs {
components.push(parse_der_string(tlv.value)?);
}
Ok(ParsedPrincipalName { components })
}
fn parse_ticket<'a>(data: &'a [u8]) -> Result<ParsedTicket<'a>> {
let (app, _) = parse_der(data)?;
if app.class != 1 || app.tag_num != 1 {
return Err(AuthError::validation("Expected Ticket (APPLICATION 1)"));
}
let (seq, _) = parse_der(app.value)?;
let fields = parse_der_contents(seq.value)?;
let vno_f =
get_ctx_field(&fields, 0).ok_or_else(|| AuthError::validation("Ticket missing tkt-vno"))?;
let vno = parse_der_integer(unwrap_explicit(vno_f)?.value)?;
if vno != 5 {
return Err(AuthError::validation(format!(
"Unsupported ticket version: {vno}"
)));
}
let realm_f =
get_ctx_field(&fields, 1).ok_or_else(|| AuthError::validation("Ticket missing realm"))?;
let realm = parse_der_string(unwrap_explicit(realm_f)?.value)?;
let sname_f =
get_ctx_field(&fields, 2).ok_or_else(|| AuthError::validation("Ticket missing sname"))?;
let sname = parse_principal_name(sname_f.value)?;
let enc_f = get_ctx_field(&fields, 3)
.ok_or_else(|| AuthError::validation("Ticket missing enc-part"))?;
let enc_part = parse_encrypted_data(enc_f.value)?;
Ok(ParsedTicket {
realm,
sname,
enc_part,
})
}
fn parse_ap_req<'a>(data: &'a [u8]) -> Result<ParsedApReq<'a>> {
let (app, _) = parse_der(data)?;
if app.class != 1 || app.tag_num != 14 {
return Err(AuthError::validation("Expected AP-REQ (APPLICATION 14)"));
}
let (seq, _) = parse_der(app.value)?;
let fields = parse_der_contents(seq.value)?;
let pvno_f =
get_ctx_field(&fields, 0).ok_or_else(|| AuthError::validation("AP-REQ missing pvno"))?;
let pvno = parse_der_integer(unwrap_explicit(pvno_f)?.value)?;
if pvno != 5 {
return Err(AuthError::validation(format!(
"Unsupported Kerberos version: {pvno}"
)));
}
let mt_f = get_ctx_field(&fields, 1)
.ok_or_else(|| AuthError::validation("AP-REQ missing msg-type"))?;
let mt = parse_der_integer(unwrap_explicit(mt_f)?.value)?;
if mt != 14 {
return Err(AuthError::validation(format!(
"Expected AP-REQ msg-type 14, got {mt}"
)));
}
let opts_f = get_ctx_field(&fields, 2)
.ok_or_else(|| AuthError::validation("AP-REQ missing ap-options"))?;
let opts_tlv = unwrap_explicit(opts_f)?;
let ap_options = parse_ap_options(&opts_tlv)?;
let ticket_f =
get_ctx_field(&fields, 3).ok_or_else(|| AuthError::validation("AP-REQ missing ticket"))?;
let ticket = parse_ticket(ticket_f.value)?;
let auth_f = get_ctx_field(&fields, 4)
.ok_or_else(|| AuthError::validation("AP-REQ missing authenticator"))?;
let authenticator = parse_encrypted_data(auth_f.value)?;
Ok(ParsedApReq {
ap_options,
ticket,
authenticator,
})
}
fn parse_ap_options(tlv: &DerTlv<'_>) -> Result<u32> {
if tlv.value.len() < 2 {
return Ok(0);
}
let mut flags: u32 = 0;
for (i, &b) in tlv.value[1..].iter().enumerate() {
if i >= 4 {
break;
}
flags |= (b as u32) << (24 - i * 8);
}
Ok(flags)
}
fn parse_enc_ticket_part(data: &[u8]) -> Result<DecryptedTicketPart> {
let (app, _) = parse_der(data)?;
if app.class != 1 || app.tag_num != 3 {
return Err(AuthError::validation(
"Expected EncTicketPart (APPLICATION 3)",
));
}
let (seq, _) = parse_der(app.value)?;
let fields = parse_der_contents(seq.value)?;
let flags_f = get_ctx_field(&fields, 0)
.ok_or_else(|| AuthError::validation("EncTicketPart missing flags"))?;
let flags_tlv = unwrap_explicit(flags_f)?;
let flags_raw = flags_tlv.value.to_vec();
let key_f = get_ctx_field(&fields, 1)
.ok_or_else(|| AuthError::validation("EncTicketPart missing key"))?;
let (key_seq, _) = parse_der(key_f.value)?;
let key_fields = parse_der_contents(key_seq.value)?;
let key_type_f = get_ctx_field(&key_fields, 0)
.ok_or_else(|| AuthError::validation("EncryptionKey missing keytype"))?;
let key_type = parse_der_integer(unwrap_explicit(key_type_f)?.value)? as i32;
let key_val_f = get_ctx_field(&key_fields, 1)
.ok_or_else(|| AuthError::validation("EncryptionKey missing keyvalue"))?;
let key_value = unwrap_explicit(key_val_f)?.value.to_vec();
let crealm_f = get_ctx_field(&fields, 2)
.ok_or_else(|| AuthError::validation("EncTicketPart missing crealm"))?;
let crealm = parse_der_string(unwrap_explicit(crealm_f)?.value)?;
let cname_f = get_ctx_field(&fields, 3)
.ok_or_else(|| AuthError::validation("EncTicketPart missing cname"))?;
let cname = parse_principal_name(cname_f.value)?;
let authtime_f = get_ctx_field(&fields, 5)
.ok_or_else(|| AuthError::validation("EncTicketPart missing authtime"))?;
let auth_time = parse_kerberos_time(unwrap_explicit(authtime_f)?.value)?;
let start_time = if let Some(f) = get_ctx_field(&fields, 6) {
Some(parse_kerberos_time(unwrap_explicit(f)?.value)?)
} else {
None
};
let endtime_f = get_ctx_field(&fields, 7)
.ok_or_else(|| AuthError::validation("EncTicketPart missing endtime"))?;
let end_time = parse_kerberos_time(unwrap_explicit(endtime_f)?.value)?;
let renew_till = if let Some(f) = get_ctx_field(&fields, 8) {
Some(parse_kerberos_time(unwrap_explicit(f)?.value)?)
} else {
None
};
Ok(DecryptedTicketPart {
flags_raw,
session_key_type: key_type,
session_key_value: key_value,
crealm,
cname,
auth_time,
end_time,
start_time,
renew_till,
})
}
fn parse_authenticator(data: &[u8]) -> Result<DecryptedAuthenticator> {
let (app, _) = parse_der(data)?;
if app.class != 1 || app.tag_num != 2 {
return Err(AuthError::validation(
"Expected Authenticator (APPLICATION 2)",
));
}
let (seq, _) = parse_der(app.value)?;
let fields = parse_der_contents(seq.value)?;
let vno_f = get_ctx_field(&fields, 0)
.ok_or_else(|| AuthError::validation("Authenticator missing vno"))?;
let vno = parse_der_integer(unwrap_explicit(vno_f)?.value)?;
if vno != 5 {
return Err(AuthError::validation(format!(
"Unsupported authenticator version: {vno}"
)));
}
let crealm_f = get_ctx_field(&fields, 1)
.ok_or_else(|| AuthError::validation("Authenticator missing crealm"))?;
let crealm = parse_der_string(unwrap_explicit(crealm_f)?.value)?;
let cname_f = get_ctx_field(&fields, 2)
.ok_or_else(|| AuthError::validation("Authenticator missing cname"))?;
let cname = parse_principal_name(cname_f.value)?;
let cusec_f = get_ctx_field(&fields, 3)
.ok_or_else(|| AuthError::validation("Authenticator missing cusec"))?;
let cusec = parse_der_integer(unwrap_explicit(cusec_f)?.value)? as u32;
let ctime_f = get_ctx_field(&fields, 4)
.ok_or_else(|| AuthError::validation("Authenticator missing ctime"))?;
let ctime = parse_kerberos_time(unwrap_explicit(ctime_f)?.value)?;
Ok(DecryptedAuthenticator {
crealm,
cname,
cusec,
ctime,
})
}
fn gcd(a: usize, b: usize) -> usize {
if b == 0 { a } else { gcd(b, a % b) }
}
fn lcm(a: usize, b: usize) -> usize {
a / gcd(a, b) * b
}
fn nfold(input: &[u8], output_len: usize) -> Vec<u8> {
let in_bytes = input.len();
let out_bytes = output_len;
let lcm_val = lcm(in_bytes, out_bytes);
let in_bits = in_bytes * 8;
let mut out = vec![0u8; out_bytes];
let mut byte: u32 = 0;
for i in (0..lcm_val).rev() {
let msbit =
((in_bits - 1) + ((in_bits + 13) * (i / in_bytes)) + ((in_bytes - i % in_bytes) * 8))
% in_bits;
let high = input[((in_bytes - 1).wrapping_sub(msbit >> 3)) % in_bytes] as u32;
let low = input[(in_bytes.wrapping_sub(msbit >> 3)) % in_bytes] as u32;
byte += ((high << 8 | low) >> ((msbit & 7) + 1)) & 0xff;
byte += out[i % out_bytes] as u32;
out[i % out_bytes] = (byte & 0xff) as u8;
byte >>= 8;
}
if byte != 0 {
for i in (0..out_bytes).rev() {
byte += out[i] as u32;
out[i] = (byte & 0xff) as u8;
byte >>= 8;
}
}
out
}
fn aes_ecb_encrypt(key: &[u8], block: &[u8; AES_BLOCK]) -> [u8; AES_BLOCK] {
let mut blk = aes::cipher::generic_array::GenericArray::clone_from_slice(block);
match key.len() {
16 => {
let cipher =
aes::Aes128::new(aes::cipher::generic_array::GenericArray::from_slice(key));
cipher.encrypt_block(&mut blk);
}
32 => {
let cipher =
aes::Aes256::new(aes::cipher::generic_array::GenericArray::from_slice(key));
cipher.encrypt_block(&mut blk);
}
_ => unreachable!("aes_ecb_encrypt: unsupported key length"),
}
let mut out = [0u8; AES_BLOCK];
out.copy_from_slice(&blk);
out
}
fn aes_ecb_decrypt(key: &[u8], block: &[u8; AES_BLOCK]) -> [u8; AES_BLOCK] {
let mut blk = aes::cipher::generic_array::GenericArray::clone_from_slice(block);
match key.len() {
16 => {
let cipher =
aes::Aes128::new(aes::cipher::generic_array::GenericArray::from_slice(key));
cipher.decrypt_block(&mut blk);
}
32 => {
let cipher =
aes::Aes256::new(aes::cipher::generic_array::GenericArray::from_slice(key));
cipher.decrypt_block(&mut blk);
}
_ => unreachable!("aes_ecb_decrypt: unsupported key length"),
}
let mut out = [0u8; AES_BLOCK];
out.copy_from_slice(&blk);
out
}
fn xor_bytes(a: &[u8], b: &[u8]) -> Vec<u8> {
a.iter().zip(b.iter()).map(|(&x, &y)| x ^ y).collect()
}
fn derive_key_aes(base_key: &[u8], usage: u32, key_type: u8) -> Vec<u8> {
let constant = [
(usage >> 24) as u8,
(usage >> 16) as u8,
(usage >> 8) as u8,
usage as u8,
key_type,
];
let nfolded = nfold(&constant, AES_BLOCK);
let key_size = base_key.len(); let mut derived = Vec::with_capacity(key_size);
let mut input: [u8; AES_BLOCK] = nfolded.try_into().expect("nfold produced wrong size");
while derived.len() < key_size {
let encrypted = aes_ecb_encrypt(base_key, &input);
derived.extend_from_slice(&encrypted);
input = encrypted;
}
derived.truncate(key_size);
derived
}
fn aes_cbc_decrypt(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
if ciphertext.len() % AES_BLOCK != 0 || ciphertext.is_empty() {
return Err(AuthError::crypto(
"AES-CBC ciphertext must be a non-empty multiple of block size",
));
}
let mut plaintext = Vec::with_capacity(ciphertext.len());
let mut prev = [0u8; AES_BLOCK];
for chunk in ciphertext.chunks_exact(AES_BLOCK) {
let ct_block: [u8; AES_BLOCK] = chunk.try_into().unwrap();
let decrypted = aes_ecb_decrypt(key, &ct_block);
let pt_block = xor_bytes(&decrypted, &prev);
plaintext.extend_from_slice(&pt_block);
prev = ct_block;
}
Ok(plaintext)
}
fn aes_cts_decrypt(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
let n = ciphertext.len();
if n < AES_BLOCK {
return Err(AuthError::crypto("AES-CTS ciphertext too short"));
}
if n == AES_BLOCK {
let ct: [u8; AES_BLOCK] = ciphertext.try_into().unwrap();
return Ok(aes_ecb_decrypt(key, &ct).to_vec());
}
if n % AES_BLOCK == 0 {
return aes_cbc_decrypt(key, ciphertext);
}
let partial_len = n % AES_BLOCK;
let num_full_blocks = n / AES_BLOCK; let preceding_len = (num_full_blocks - 1) * AES_BLOCK;
let c_second_last: [u8; AES_BLOCK] = ciphertext[preceding_len..preceding_len + AES_BLOCK]
.try_into()
.unwrap();
let c_last = &ciphertext[preceding_len + AES_BLOCK..];
let prev_cipher = if preceding_len >= AES_BLOCK {
&ciphertext[preceding_len - AES_BLOCK..preceding_len]
} else {
&[0u8; AES_BLOCK][..] };
let d = aes_ecb_decrypt(key, &c_second_last);
let p_last = xor_bytes(&d[..partial_len], c_last);
let mut c_recovered = [0u8; AES_BLOCK];
c_recovered[..partial_len].copy_from_slice(c_last);
c_recovered[partial_len..].copy_from_slice(&d[partial_len..]);
let decrypted_recovered = aes_ecb_decrypt(key, &c_recovered);
let p_second_last = xor_bytes(&decrypted_recovered, prev_cipher);
let mut plaintext = if preceding_len > 0 {
aes_cbc_decrypt(key, &ciphertext[..preceding_len])?
} else {
Vec::new()
};
plaintext.extend_from_slice(&p_second_last);
plaintext.extend_from_slice(&p_last);
Ok(plaintext)
}
fn hmac_sha1(key: &[u8], data: &[u8]) -> Vec<u8> {
use hmac::{Hmac, Mac};
type HmacSha1 = Hmac<sha1::Sha1>;
let mut mac = <HmacSha1 as Mac>::new_from_slice(key).expect("HMAC-SHA1 accepts any key length");
mac.update(data);
mac.finalize().into_bytes().to_vec()
}
fn decrypt_aes_cts(base_key: &[u8], ciphertext: &[u8], etype: i32, usage: u32) -> Result<Vec<u8>> {
let expected_key_len = match etype {
ETYPE_AES128 => 16,
ETYPE_AES256 => 32,
_ => {
return Err(AuthError::crypto(format!(
"Unsupported encryption type {etype}; only AES etypes 17/18 are supported"
)));
}
};
if base_key.len() != expected_key_len {
return Err(AuthError::crypto(format!(
"Key length mismatch: expected {expected_key_len}, got {}",
base_key.len()
)));
}
if ciphertext.len() < CONFOUNDER_LEN + HMAC_LEN {
return Err(AuthError::crypto(
"Ciphertext too short for AES-CTS envelope",
));
}
let ct_body = &ciphertext[..ciphertext.len() - HMAC_LEN];
let expected_hmac = &ciphertext[ciphertext.len() - HMAC_LEN..];
let ke = derive_key_aes(base_key, usage, 0xAA);
let ki = derive_key_aes(base_key, usage, 0x55);
let plaintext_with_confounder = aes_cts_decrypt(&ke, ct_body)?;
let computed_hmac = hmac_sha1(&ki, &plaintext_with_confounder);
if computed_hmac[..HMAC_LEN].ct_eq(expected_hmac).unwrap_u8() != 1 {
return Err(AuthError::crypto(
"Kerberos HMAC verification failed — wrong key or corrupted ticket",
));
}
Ok(plaintext_with_confounder[CONFOUNDER_LEN..].to_vec())
}
fn parse_spnego_init_token(data: &[u8]) -> Result<SpnegoToken> {
let (app, _) = parse_der(data)?;
if app.class != 1 || app.tag_num != 0 {
return Err(AuthError::validation(
"Expected GSS-API InitialContextToken (APPLICATION 0)",
));
}
let (oid_tlv, rest) = parse_der(app.value)?;
if !oid_matches(&oid_tlv, SPNEGO_OID_BYTES) {
return Err(AuthError::validation("Not an SPNEGO token"));
}
let (neg_init_wrapper, _) = parse_der(rest)?;
if neg_init_wrapper.class != 2 || neg_init_wrapper.tag_num != 0 {
return Err(AuthError::validation("Expected NegTokenInit ([CONTEXT 0])"));
}
let (neg_init_seq, _) = parse_der(neg_init_wrapper.value)?;
let fields = parse_der_contents(neg_init_seq.value)?;
let mech_types_f = get_ctx_field(&fields, 0)
.ok_or_else(|| AuthError::validation("NegTokenInit missing mechTypes"))?;
let (mech_seq, _) = parse_der(mech_types_f.value)?;
let mech_oids = parse_der_contents(mech_seq.value)?;
let has_krb5 = mech_oids.iter().any(|o| oid_matches(o, KRB5_OID_BYTES));
if !has_krb5 {
return Err(AuthError::validation(
"SPNEGO NegTokenInit does not offer Kerberos 5",
));
}
let mech_token_f = get_ctx_field(&fields, 2)
.ok_or_else(|| AuthError::validation("NegTokenInit missing mechToken"))?;
let mech_token_tlv = unwrap_explicit(mech_token_f)?;
Ok(SpnegoToken {
mech_oid: oid::KERBEROS_V5.to_string(),
mech_token: mech_token_tlv.value.to_vec(),
state: SpnegoState::Initial,
})
}
fn parse_spnego_resp_token(data: &[u8]) -> Result<SpnegoToken> {
let (wrapper, _) = parse_der(data)?;
if wrapper.class != 2 || wrapper.tag_num != 1 {
return Err(AuthError::validation("Expected NegTokenResp ([CONTEXT 1])"));
}
let (seq, _) = parse_der(wrapper.value)?;
let fields = parse_der_contents(seq.value)?;
let mech_token = if let Some(f) = get_ctx_field(&fields, 2) {
unwrap_explicit(f)?.value.to_vec()
} else {
Vec::new()
};
Ok(SpnegoToken {
mech_oid: oid::KERBEROS_V5.to_string(),
mech_token,
state: SpnegoState::Continue,
})
}
#[derive(Debug, Clone)]
pub struct KerberosConfig {
pub service_principal: String,
pub realm: String,
pub keytab_path: Option<String>,
pub kdc_addresses: Vec<String>,
pub max_clock_skew_secs: u64,
pub allow_delegation: bool,
pub replay_cache_max_entries: usize,
}
impl Default for KerberosConfig {
fn default() -> Self {
Self {
service_principal: String::new(),
realm: String::new(),
keytab_path: None,
kdc_addresses: Vec::new(),
max_clock_skew_secs: 300,
allow_delegation: false,
replay_cache_max_entries: 100_000,
}
}
}
impl KerberosConfig {
pub fn builder(
service_principal: impl Into<String>,
realm: impl Into<String>,
) -> KerberosConfigBuilder {
KerberosConfigBuilder {
config: KerberosConfig {
service_principal: service_principal.into(),
realm: realm.into(),
..Default::default()
},
}
}
pub fn active_directory(
service_principal: impl Into<String>,
realm: impl Into<String>,
) -> Self {
Self {
service_principal: service_principal.into(),
realm: realm.into(),
allow_delegation: true,
..Default::default()
}
}
}
pub struct KerberosConfigBuilder {
config: KerberosConfig,
}
impl KerberosConfigBuilder {
pub fn keytab_path(mut self, path: impl Into<String>) -> Self {
self.config.keytab_path = Some(path.into());
self
}
pub fn add_kdc(mut self, addr: impl Into<String>) -> Self {
self.config.kdc_addresses.push(addr.into());
self
}
pub fn max_clock_skew_secs(mut self, secs: u64) -> Self {
self.config.max_clock_skew_secs = secs;
self
}
pub fn allow_delegation(mut self, allow: bool) -> Self {
self.config.allow_delegation = allow;
self
}
pub fn replay_cache_max_entries(mut self, max: usize) -> Self {
self.config.replay_cache_max_entries = max;
self
}
pub fn build(self) -> KerberosConfig {
self.config
}
}
pub mod oid {
pub const SPNEGO: &str = "1.3.6.1.5.5.2";
pub const KERBEROS_V5: &str = "1.2.840.113554.1.2.2";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KerberosAuthResult {
pub client_principal: String,
pub realm: String,
pub auth_time: u64,
pub end_time: u64,
pub is_delegated: bool,
pub flags: KerberosTicketFlags,
pub response_token: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct KerberosTicketFlags {
pub forwardable: bool,
pub forwarded: bool,
pub proxiable: bool,
pub proxy: bool,
pub may_postdate: bool,
pub postdated: bool,
pub renewable: bool,
pub pre_authent: bool,
pub hw_authent: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SpnegoState {
Initial,
Continue,
Completed,
Rejected,
}
#[derive(Debug, Clone)]
pub struct SpnegoToken {
pub mech_oid: String,
pub mech_token: Vec<u8>,
pub state: SpnegoState,
}
#[derive(Debug, Clone)]
pub struct KeytabEntry {
pub principal: String,
pub realm: String,
pub kvno: u32,
pub key_type: u32,
pub key_data: Vec<u8>,
}
#[derive(Debug)]
pub struct KerberosManager {
config: KerberosConfig,
replay_cache: Arc<RwLock<HashMap<Vec<u8>, u64>>>,
keytab_entries: Arc<RwLock<Vec<KeytabEntry>>>,
}
impl KerberosManager {
pub fn new(config: KerberosConfig) -> Result<Self> {
if config.service_principal.is_empty() {
return Err(AuthError::config("Kerberos service principal must be set"));
}
if config.realm.is_empty() {
return Err(AuthError::config("Kerberos realm must be set"));
}
Ok(Self {
config,
replay_cache: Arc::new(RwLock::new(HashMap::new())),
keytab_entries: Arc::new(RwLock::new(Vec::new())),
})
}
pub async fn load_keytab(&self, path: &str) -> Result<usize> {
let data = tokio::fs::read(path)
.await
.map_err(|e| AuthError::config(format!("Failed to read keytab file: {e}")))?;
let entries = parse_keytab(&data)?;
let count = entries.len();
let mut kt = self.keytab_entries.write().await;
*kt = entries;
Ok(count)
}
pub async fn authenticate(&self, negotiate_token: &str) -> Result<KerberosAuthResult> {
let token_bytes = base64::engine::general_purpose::STANDARD
.decode(negotiate_token.trim())
.map_err(|e| AuthError::validation(format!("Invalid Negotiate token encoding: {e}")))?;
if token_bytes.is_empty() {
return Err(AuthError::validation("Empty Negotiate token"));
}
let spnego = self.parse_spnego_token(&token_bytes)?;
if spnego.mech_oid != oid::KERBEROS_V5 {
return Err(AuthError::validation(format!(
"Unsupported SPNEGO mechanism: {}",
spnego.mech_oid
)));
}
let result = self.validate_ap_req(&spnego.mech_token).await?;
Ok(result)
}
pub fn generate_challenge(&self) -> String {
"Negotiate".to_string()
}
fn parse_spnego_token(&self, data: &[u8]) -> Result<SpnegoToken> {
if data.len() < 2 {
return Err(AuthError::validation("SPNEGO token too short"));
}
match data[0] {
0x60 => parse_spnego_init_token(data),
0xa1 => parse_spnego_resp_token(data),
_ => Ok(SpnegoToken {
mech_oid: oid::KERBEROS_V5.to_string(),
mech_token: data.to_vec(),
state: SpnegoState::Initial,
}),
}
}
async fn validate_ap_req(&self, ap_req_bytes: &[u8]) -> Result<KerberosAuthResult> {
let keytab = self.keytab_entries.read().await;
if keytab.is_empty() {
return Err(AuthError::config(
"No keytab loaded — cannot validate Kerberos tickets",
));
}
let ap_req = parse_ap_req(ap_req_bytes)?;
let ticket_etype = ap_req.ticket.enc_part.etype;
let ticket_kvno = ap_req.ticket.enc_part.kvno;
let ticket_sname = format!(
"{}@{}",
ap_req.ticket.sname.to_string_without_realm(),
ap_req.ticket.realm
);
let entry = keytab
.iter()
.find(|e| {
e.principal == ticket_sname
&& e.key_type == ticket_etype as u32
&& ticket_kvno.is_none_or(|v| e.kvno == v)
})
.or_else(|| {
keytab
.iter()
.find(|e| e.realm == ap_req.ticket.realm && e.key_type == ticket_etype as u32)
})
.ok_or_else(|| {
AuthError::config(format!(
"No keytab entry for principal={ticket_sname} etype={ticket_etype}"
))
})?;
let ticket_plaintext = decrypt_aes_cts(
&entry.key_data,
ap_req.ticket.enc_part.cipher,
ticket_etype,
KEY_USAGE_TICKET,
)?;
let ticket_part = parse_enc_ticket_part(&ticket_plaintext)?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| AuthError::internal(format!("Clock error: {e}")))?
.as_secs();
if now > ticket_part.end_time + self.config.max_clock_skew_secs {
return Err(AuthError::validation("Kerberos ticket has expired"));
}
if let Some(start) = ticket_part.start_time {
if now + self.config.max_clock_skew_secs < start {
return Err(AuthError::validation("Kerberos ticket is not yet valid"));
}
}
let auth_etype = ap_req.authenticator.etype;
let auth_plaintext = decrypt_aes_cts(
&ticket_part.session_key_value,
ap_req.authenticator.cipher,
auth_etype,
KEY_USAGE_AP_REQ_AUTH,
)?;
let authenticator = parse_authenticator(&auth_plaintext)?;
if authenticator.crealm != ticket_part.crealm {
return Err(AuthError::validation(
"Authenticator crealm does not match ticket",
));
}
if authenticator.cname.to_string_without_realm()
!= ticket_part.cname.to_string_without_realm()
{
return Err(AuthError::validation(
"Authenticator cname does not match ticket",
));
}
let time_diff = if now > authenticator.ctime {
now - authenticator.ctime
} else {
authenticator.ctime - now
};
if time_diff > self.config.max_clock_skew_secs {
return Err(AuthError::validation(format!(
"Authenticator clock skew too large: {time_diff}s (max {}s)",
self.config.max_clock_skew_secs
)));
}
self.check_replay_authenticator(
authenticator.ctime,
authenticator.cusec,
&authenticator.cname.to_string_without_realm(),
)
.await?;
let client_principal = format!(
"{}@{}",
ticket_part.cname.to_string_without_realm(),
ticket_part.crealm
);
let is_delegated = test_bit_flag(&ticket_part.flags_raw, 2);
let flags = KerberosTicketFlags {
forwardable: test_bit_flag(&ticket_part.flags_raw, 1),
forwarded: test_bit_flag(&ticket_part.flags_raw, 2),
proxiable: test_bit_flag(&ticket_part.flags_raw, 3),
proxy: test_bit_flag(&ticket_part.flags_raw, 4),
may_postdate: test_bit_flag(&ticket_part.flags_raw, 5),
postdated: test_bit_flag(&ticket_part.flags_raw, 6),
renewable: test_bit_flag(&ticket_part.flags_raw, 8),
pre_authent: test_bit_flag(&ticket_part.flags_raw, 10),
hw_authent: test_bit_flag(&ticket_part.flags_raw, 11),
};
if is_delegated && !self.config.allow_delegation {
return Err(AuthError::validation(
"Delegated (forwarded) tickets are not allowed by policy",
));
}
Ok(KerberosAuthResult {
client_principal,
realm: ticket_part.crealm,
auth_time: ticket_part.auth_time,
end_time: ticket_part.end_time,
is_delegated,
flags,
response_token: None,
})
}
async fn check_replay_authenticator(&self, ctime: u64, cusec: u32, cname: &str) -> Result<()> {
let mut hasher = sha2::Sha256::new();
hasher.update(ctime.to_be_bytes());
hasher.update(cusec.to_be_bytes());
hasher.update(cname.as_bytes());
let hash = hasher.finalize().to_vec();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| AuthError::internal(format!("Clock error: {e}")))?
.as_secs();
let mut cache = self.replay_cache.write().await;
let cutoff = now.saturating_sub(self.config.max_clock_skew_secs * 2);
cache.retain(|_, &mut ts| ts > cutoff);
if cache.contains_key(&hash) {
return Err(AuthError::validation("Kerberos replay attack detected"));
}
if cache.len() >= self.config.replay_cache_max_entries {
return Err(AuthError::internal("Kerberos replay cache full"));
}
cache.insert(hash, now);
Ok(())
}
pub async fn check_replay(&self, token_data: &[u8]) -> Result<()> {
let hash = sha2::Sha256::digest(token_data).to_vec();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| AuthError::internal(format!("Clock error: {e}")))?
.as_secs();
let mut cache = self.replay_cache.write().await;
let cutoff = now.saturating_sub(self.config.max_clock_skew_secs * 2);
cache.retain(|_, &mut ts| ts > cutoff);
if cache.contains_key(&hash) {
return Err(AuthError::validation("Kerberos replay attack detected"));
}
if cache.len() >= self.config.replay_cache_max_entries {
return Err(AuthError::internal("Kerberos replay cache full"));
}
cache.insert(hash, now);
Ok(())
}
#[allow(dead_code)]
fn generate_response_token(&self) -> Result<Option<String>> {
let rng = ring::rand::SystemRandom::new();
let mut nonce = [0u8; 16];
rng.fill(&mut nonce)
.map_err(|_| AuthError::crypto("Failed to generate SPNEGO response nonce"))?;
let encoded = base64::engine::general_purpose::STANDARD.encode(nonce);
Ok(Some(encoded))
}
}
fn parse_keytab(data: &[u8]) -> Result<Vec<KeytabEntry>> {
if data.len() < 4 {
return Err(AuthError::config("Keytab file too short"));
}
let version = u16::from_be_bytes([data[0], data[1]]);
if version != 0x0502 && version != 0x0501 {
return Err(AuthError::config(format!(
"Unsupported keytab version: 0x{version:04x}"
)));
}
let mut entries = Vec::new();
let mut pos = 2;
while pos + 4 <= data.len() {
let entry_len =
i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
pos += 4;
if entry_len <= 0 {
pos += entry_len.unsigned_abs() as usize;
continue;
}
let entry_len = entry_len as usize;
if pos + entry_len > data.len() {
break;
}
let entry_data = &data[pos..pos + entry_len];
if let Ok(entry) = parse_keytab_entry(entry_data, version) {
entries.push(entry);
}
pos += entry_len;
}
Ok(entries)
}
fn parse_keytab_entry(data: &[u8], _version: u16) -> Result<KeytabEntry> {
if data.len() < 8 {
return Err(AuthError::config("Keytab entry too short"));
}
let num_components = u16::from_be_bytes([data[0], data[1]]) as usize;
let mut pos = 2;
if pos + 2 > data.len() {
return Err(AuthError::config("Keytab entry truncated at realm length"));
}
let realm_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if pos + realm_len > data.len() {
return Err(AuthError::config("Keytab entry truncated at realm data"));
}
let realm = String::from_utf8_lossy(&data[pos..pos + realm_len]).to_string();
pos += realm_len;
let mut principal_parts = Vec::new();
for _ in 0..num_components {
if pos + 2 > data.len() {
return Err(AuthError::config("Keytab entry truncated at component"));
}
let comp_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if pos + comp_len > data.len() {
return Err(AuthError::config(
"Keytab entry truncated at component data",
));
}
principal_parts.push(String::from_utf8_lossy(&data[pos..pos + comp_len]).to_string());
pos += comp_len;
}
let principal = format!("{}@{}", principal_parts.join("/"), realm);
pos += 8;
if pos >= data.len() {
return Err(AuthError::config("Keytab entry truncated at kvno"));
}
let kvno = data.get(pos).copied().unwrap_or(0) as u32;
pos += 1;
if pos + 2 > data.len() {
return Err(AuthError::config("Keytab entry truncated at key type"));
}
let key_type = u16::from_be_bytes([data[pos], data[pos + 1]]) as u32;
pos += 2;
if pos + 2 > data.len() {
return Err(AuthError::config("Keytab entry truncated at key length"));
}
let key_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if pos + key_len > data.len() {
return Err(AuthError::config("Keytab entry truncated at key data"));
}
let key_data = data[pos..pos + key_len].to_vec();
Ok(KeytabEntry {
principal,
realm,
kvno,
key_type,
key_data,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_defaults() {
let config = KerberosConfig::default();
assert_eq!(config.max_clock_skew_secs, 300);
assert!(!config.allow_delegation);
}
#[test]
fn test_manager_requires_principal() {
let config = KerberosConfig::default();
let err = KerberosManager::new(config).unwrap_err();
assert!(err.to_string().contains("service principal"));
}
#[test]
fn test_manager_requires_realm() {
let config = KerberosConfig {
service_principal: "HTTP/server.example.com".into(),
..Default::default()
};
let err = KerberosManager::new(config).unwrap_err();
assert!(err.to_string().contains("realm"));
}
#[test]
fn test_manager_creation() {
let config = KerberosConfig {
service_principal: "HTTP/server.example.com@EXAMPLE.COM".into(),
realm: "EXAMPLE.COM".into(),
..Default::default()
};
let mgr = KerberosManager::new(config);
assert!(mgr.is_ok());
}
#[test]
fn test_generate_challenge() {
let config = KerberosConfig {
service_principal: "HTTP/server.example.com@EXAMPLE.COM".into(),
realm: "EXAMPLE.COM".into(),
..Default::default()
};
let mgr = KerberosManager::new(config).unwrap();
assert_eq!(mgr.generate_challenge(), "Negotiate");
}
#[tokio::test]
async fn test_replay_detection() {
let config = KerberosConfig {
service_principal: "HTTP/server.example.com@EXAMPLE.COM".into(),
realm: "EXAMPLE.COM".into(),
..Default::default()
};
let mgr = KerberosManager::new(config).unwrap();
let token_data = b"test_token_data";
mgr.check_replay(token_data).await.unwrap();
let err = mgr.check_replay(token_data).await.unwrap_err();
assert!(err.to_string().contains("replay"));
}
#[test]
fn test_invalid_keytab() {
let result = parse_keytab(&[0x00, 0x01]);
assert!(result.is_err());
}
#[test]
fn test_spnego_state_variants() {
assert_eq!(SpnegoState::Initial, SpnegoState::Initial);
assert_ne!(SpnegoState::Initial, SpnegoState::Completed);
}
#[test]
fn test_parse_der_integer() {
let data = [0x02, 0x01, 0x05];
let (tlv, rest) = parse_der(&data).unwrap();
assert!(rest.is_empty());
assert_eq!(tlv.class, 0); assert_eq!(tlv.tag_num, 2); assert_eq!(parse_der_integer(tlv.value).unwrap(), 5);
}
#[test]
fn test_parse_der_sequence() {
let data = [0x30, 0x06, 0x02, 0x01, 0x05, 0x02, 0x01, 0x0e];
let (tlv, _) = parse_der(&data).unwrap();
assert_eq!(tlv.tag_num, 16); assert!(tlv.constructed);
let contents = parse_der_contents(tlv.value).unwrap();
assert_eq!(contents.len(), 2);
assert_eq!(parse_der_integer(contents[0].value).unwrap(), 5);
assert_eq!(parse_der_integer(contents[1].value).unwrap(), 14);
}
#[test]
fn test_parse_der_context_tag() {
let data = [0xa0, 0x03, 0x02, 0x01, 0x05];
let (tlv, _) = parse_der(&data).unwrap();
assert_eq!(tlv.class, 2); assert_eq!(tlv.tag_num, 0);
let inner = unwrap_explicit(&tlv).unwrap();
assert_eq!(parse_der_integer(inner.value).unwrap(), 5);
}
#[test]
fn test_parse_der_oid() {
let data = [0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
let (tlv, _) = parse_der(&data).unwrap();
assert!(oid_matches(&tlv, SPNEGO_OID_BYTES));
assert!(!oid_matches(&tlv, KRB5_OID_BYTES));
}
#[test]
fn test_parse_der_truncated_fails() {
assert!(parse_der(&[]).is_err());
assert!(parse_der(&[0x02]).is_err()); assert!(parse_der(&[0x02, 0x05, 0x01]).is_err()); }
#[test]
fn test_nfold_64bit() {
let result = nfold(b"012345", 8);
assert_eq!(result, vec![0xBE, 0x07, 0x26, 0x31, 0x27, 0x6B, 0x19, 0x55]);
}
#[test]
fn test_nfold_56bit() {
let result = nfold(b"password", 7);
assert_eq!(result, vec![0x78, 0xA0, 0x7B, 0x6C, 0xAF, 0x85, 0xFA]);
}
#[test]
fn test_nfold_64bit_long_input() {
let result = nfold(b"Rough Consensus, and Running Code", 8);
assert_eq!(result, vec![0xBB, 0x6E, 0xD3, 0x08, 0x70, 0xB7, 0xF0, 0xE0]);
}
#[test]
fn test_nfold_168bit() {
let result = nfold(b"password", 21);
assert_eq!(
result,
vec![
0x59, 0xE4, 0xA8, 0xCA, 0x7C, 0x03, 0x85, 0xC3, 0xC3, 0x7B, 0x3F, 0x6D, 0x20, 0x00,
0x24, 0x7C, 0xB6, 0xE6, 0xBD, 0x5B, 0x3E,
]
);
}
#[test]
fn test_aes_ecb_roundtrip() {
let key = [0x42u8; 16];
let block = [0x01u8; 16];
let encrypted = aes_ecb_encrypt(&key, &block);
let decrypted = aes_ecb_decrypt(&key, &encrypted);
assert_eq!(decrypted, block);
}
#[test]
fn test_aes_ecb_256_roundtrip() {
let key = [0x42u8; 32];
let block = [0xABu8; 16];
let encrypted = aes_ecb_encrypt(&key, &block);
let decrypted = aes_ecb_decrypt(&key, &encrypted);
assert_eq!(decrypted, block);
}
#[test]
fn test_aes_cbc_decrypt_two_blocks() {
let key = [0x00u8; 16];
let p0 = [0u8; 16];
let p1 = [0u8; 16];
let c0 = aes_ecb_encrypt(&key, &p0);
let p1_xor_c0: [u8; 16] = xor_bytes(&p1, &c0).try_into().unwrap();
let c1 = aes_ecb_encrypt(&key, &p1_xor_c0);
let mut ciphertext = Vec::new();
ciphertext.extend_from_slice(&c0);
ciphertext.extend_from_slice(&c1);
let plaintext = aes_cbc_decrypt(&key, &ciphertext).unwrap();
let mut expected = Vec::new();
expected.extend_from_slice(&p0);
expected.extend_from_slice(&p1);
assert_eq!(plaintext, expected);
}
#[test]
fn test_aes_cts_decrypt_single_block() {
let key = [0x00u8; 16];
let plaintext = [0x42u8; 16];
let ciphertext = aes_ecb_encrypt(&key, &plaintext);
let decrypted = aes_cts_decrypt(&key, &ciphertext).unwrap();
assert_eq!(&decrypted[..], &plaintext[..]);
}
#[test]
fn test_derive_key_produces_correct_length() {
let key_128 = [0x42u8; 16];
let derived = derive_key_aes(&key_128, 2, 0xAA);
assert_eq!(derived.len(), 16);
let key_256 = [0x42u8; 32];
let derived = derive_key_aes(&key_256, 2, 0xAA);
assert_eq!(derived.len(), 32);
}
#[test]
fn test_hmac_sha1_produces_20_bytes() {
let result = hmac_sha1(b"key", b"data");
assert_eq!(result.len(), 20);
}
#[test]
fn test_valid_keytab_v2_header() {
let data = [0x05, 0x02, 0x00, 0x00];
let entries = parse_keytab(&data).unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_ticket_flags_parsing() {
let flags = [0x00, 0x40, 0x80, 0x00, 0x00]; assert!(test_bit_flag(&flags, 1)); assert!(!test_bit_flag(&flags, 2)); assert!(test_bit_flag(&flags, 8)); assert!(!test_bit_flag(&flags, 10)); }
#[test]
fn test_kerberos_config_builder() {
let config = KerberosConfig::builder("HTTP/srv@REALM", "REALM")
.keytab_path("/etc/krb5.keytab")
.add_kdc("kdc1:88")
.add_kdc("kdc2:88")
.max_clock_skew_secs(600)
.build();
assert_eq!(config.service_principal, "HTTP/srv@REALM");
assert_eq!(config.realm, "REALM");
assert_eq!(config.keytab_path.as_deref(), Some("/etc/krb5.keytab"));
assert_eq!(config.kdc_addresses, vec!["kdc1:88", "kdc2:88"]);
assert_eq!(config.max_clock_skew_secs, 600);
assert!(!config.allow_delegation);
}
#[test]
fn test_kerberos_config_active_directory() {
let config = KerberosConfig::active_directory("HTTP/srv@AD.COM", "AD.COM");
assert_eq!(config.service_principal, "HTTP/srv@AD.COM");
assert_eq!(config.realm, "AD.COM");
assert!(config.allow_delegation);
let mgr = KerberosManager::new(config);
assert!(mgr.is_ok());
}
#[test]
fn test_kerberos_builder_override() {
let config = KerberosConfig::builder("HTTP/srv@REALM", "REALM")
.allow_delegation(true)
.replay_cache_max_entries(500)
.build();
assert!(config.allow_delegation);
assert_eq!(config.replay_cache_max_entries, 500);
}
}