Skip to main content

auth_framework/protocols/
kerberos.rs

1//! Kerberos / SPNEGO Authentication Protocol Support
2//!
3//! Implements Kerberos 5 (RFC 4120) ticket validation and SPNEGO (RFC 4178)
4//! negotiation for HTTP-based authentication, enabling seamless SSO with
5//! Active Directory and MIT Kerberos environments.
6//!
7//! # Supported Encryption Types
8//!
9//! - AES256-CTS-HMAC-SHA1-96 (etype 18) — recommended
10//! - AES128-CTS-HMAC-SHA1-96 (etype 17) — fallback
11//!
12//! # Architecture
13//!
14//! This module operates as a **service-side ticket validator**:
15//!
16//! 1. Client sends an `Authorization: Negotiate <token>` header
17//! 2. Server decodes the SPNEGO wrapper and extracts the Kerberos AP-REQ
18//! 3. Server decrypts the ticket using keytab keys (AES-CTS-HMAC-SHA1-96)
19//! 4. Server decrypts and verifies the authenticator using the session key
20//! 5. On success, extracts the client principal and returns an auth result
21//!
22//! # Security Considerations
23//!
24//! - Keytab files must be protected with strict filesystem permissions
25//! - Replay protection uses a time-windowed nonce cache
26//! - Clock skew tolerance is configurable (default: 5 minutes per RFC 4120)
27//! - All cryptographic comparisons use constant-time operations
28
29use crate::errors::{AuthError, Result};
30use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
31use base64::Engine as _;
32use ring::rand::SecureRandom;
33use serde::{Deserialize, Serialize};
34use sha2::Digest;
35use std::collections::HashMap;
36use std::sync::Arc;
37use std::time::{SystemTime, UNIX_EPOCH};
38use subtle::ConstantTimeEq;
39use tokio::sync::RwLock;
40
41// ─── Constants ───────────────────────────────────────────────────────────────
42
43/// Encryption type: AES128-CTS-HMAC-SHA1-96 (RFC 3962).
44const ETYPE_AES128: i32 = 17;
45/// Encryption type: AES256-CTS-HMAC-SHA1-96 (RFC 3962).
46const ETYPE_AES256: i32 = 18;
47
48/// Key usage for ticket encryption (RFC 4120 §7.5.1).
49const KEY_USAGE_TICKET: u32 = 2;
50/// Key usage for AP-REQ authenticator (RFC 4120 §7.5.1, for AES etypes).
51const KEY_USAGE_AP_REQ_AUTH: u32 = 11;
52
53/// AES block size.
54const AES_BLOCK: usize = 16;
55/// HMAC-SHA1-96 output length (truncated to 96 bits = 12 bytes).
56const HMAC_LEN: usize = 12;
57/// Confounder length (one AES block).
58const CONFOUNDER_LEN: usize = 16;
59
60/// SPNEGO OID bytes (1.3.6.1.5.5.2) as encoded in DER (after tag+length).
61const SPNEGO_OID_BYTES: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
62/// Kerberos 5 OID bytes (1.2.840.113554.1.2.2) as encoded in DER.
63const KRB5_OID_BYTES: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02];
64
65// ─── Minimal DER Parser ──────────────────────────────────────────────────────
66
67/// A parsed DER Tag-Length-Value.
68#[allow(dead_code)]
69#[derive(Debug)]
70struct DerTlv<'a> {
71    /// Tag class: 0=Universal, 1=Application, 2=Context-specific, 3=Private.
72    class: u8,
73    /// Whether this is a constructed encoding.
74    constructed: bool,
75    /// Tag number.
76    tag_num: u32,
77    /// The value bytes (content octets).
78    value: &'a [u8],
79}
80
81/// Parse one DER TLV from the front of `data`. Returns (tlv, remaining_bytes).
82fn parse_der(data: &[u8]) -> Result<(DerTlv<'_>, &[u8])> {
83    if data.is_empty() {
84        return Err(AuthError::validation("Empty DER data"));
85    }
86
87    let b0 = data[0];
88    let class = b0 >> 6;
89    let constructed = (b0 & 0x20) != 0;
90    let mut pos: usize = 1;
91
92    // Tag number
93    let tag_num = if (b0 & 0x1f) == 0x1f {
94        let mut t: u32 = 0;
95        loop {
96            if pos >= data.len() {
97                return Err(AuthError::validation("DER tag truncated"));
98            }
99            let b = data[pos];
100            pos += 1;
101            t = t
102                .checked_shl(7)
103                .ok_or_else(|| AuthError::validation("DER tag too large"))?
104                | (b & 0x7f) as u32;
105            if (b & 0x80) == 0 {
106                break;
107            }
108        }
109        t
110    } else {
111        (b0 & 0x1f) as u32
112    };
113
114    // Length
115    if pos >= data.len() {
116        return Err(AuthError::validation("DER length missing"));
117    }
118    let len_byte = data[pos];
119    pos += 1;
120
121    let length = if len_byte < 0x80 {
122        len_byte as usize
123    } else if len_byte == 0x80 {
124        return Err(AuthError::validation(
125            "Indefinite length not supported in DER",
126        ));
127    } else {
128        let num_bytes = (len_byte & 0x7f) as usize;
129        if num_bytes > 4 || pos + num_bytes > data.len() {
130            return Err(AuthError::validation("DER length overflow"));
131        }
132        let mut l: usize = 0;
133        for &b in &data[pos..pos + num_bytes] {
134            l = l
135                .checked_shl(8)
136                .ok_or_else(|| AuthError::validation("DER length too large"))?
137                | b as usize;
138        }
139        pos += num_bytes;
140        l
141    };
142
143    if pos + length > data.len() {
144        return Err(AuthError::validation("DER value truncated"));
145    }
146
147    Ok((
148        DerTlv {
149            class,
150            constructed,
151            tag_num,
152            value: &data[pos..pos + length],
153        },
154        &data[pos + length..],
155    ))
156}
157
158/// Parse all consecutive TLVs from the content of a constructed DER object.
159fn parse_der_contents(data: &[u8]) -> Result<Vec<DerTlv<'_>>> {
160    let mut result = Vec::new();
161    let mut remaining = data;
162    while !remaining.is_empty() {
163        let (tlv, rest) = parse_der(remaining)?;
164        result.push(tlv);
165        remaining = rest;
166    }
167    Ok(result)
168}
169
170/// Find a context-tagged field ([n] EXPLICIT) in a list of DER TLVs.
171fn get_ctx_field<'a, 'b>(fields: &'b [DerTlv<'a>], tag: u32) -> Option<&'b DerTlv<'a>> {
172    fields.iter().find(|f| f.class == 2 && f.tag_num == tag)
173}
174
175/// Unwrap an explicit context tag: parse the inner TLV from the tag's value.
176fn unwrap_explicit<'a>(tlv: &DerTlv<'a>) -> Result<DerTlv<'a>> {
177    let (inner, _) = parse_der(tlv.value)?;
178    Ok(inner)
179}
180
181/// Parse a DER INTEGER value as i64.
182fn parse_der_integer(data: &[u8]) -> Result<i64> {
183    if data.is_empty() {
184        return Err(AuthError::validation("Empty INTEGER"));
185    }
186    let mut val: i64 = if data[0] & 0x80 != 0 { -1 } else { 0 };
187    for &b in data {
188        val = (val << 8) | b as i64;
189    }
190    Ok(val)
191}
192
193/// Parse a DER GeneralString / GeneralizedTime / IA5String as UTF-8.
194fn parse_der_string(data: &[u8]) -> Result<String> {
195    String::from_utf8(data.to_vec())
196        .map_err(|e| AuthError::validation(format!("Invalid string encoding: {e}")))
197}
198
199/// Parse KerberosTime (GeneralizedTime: "YYYYMMDDHHmmSSZ") to Unix timestamp.
200fn parse_kerberos_time(data: &[u8]) -> Result<u64> {
201    let s = parse_der_string(data)?;
202    if s.len() < 15 || !s.ends_with('Z') {
203        return Err(AuthError::validation("Invalid KerberosTime format"));
204    }
205    let dt = chrono::NaiveDateTime::parse_from_str(&s[..14], "%Y%m%d%H%M%S")
206        .map_err(|e| AuthError::validation(format!("Invalid KerberosTime: {e}")))?;
207    Ok(dt.and_utc().timestamp() as u64)
208}
209
210/// Test a single bit in a BIT STRING value (after the unused-bits byte).
211/// Bit numbering follows ASN.1: bit 0 is the MSB of the first content octet.
212fn test_bit_flag(flags_data: &[u8], bit_num: usize) -> bool {
213    if flags_data.is_empty() {
214        return false;
215    }
216    // First byte is unused-bits count; actual flag bytes start at index 1
217    let byte_idx = 1 + bit_num / 8;
218    let bit_idx = 7 - (bit_num % 8);
219    if byte_idx >= flags_data.len() {
220        return false;
221    }
222    (flags_data[byte_idx] >> bit_idx) & 1 == 1
223}
224
225/// Compare a DER OID's value bytes against a known OID byte pattern.
226fn oid_matches(tlv: &DerTlv<'_>, expected: &[u8]) -> bool {
227    tlv.class == 0 && tlv.tag_num == 6 && tlv.value == expected
228}
229
230// ─── Kerberos ASN.1 Structures ───────────────────────────────────────────────
231
232/// Parsed EncryptedData (RFC 4120 §5.2.9).
233struct ParsedEncryptedData<'a> {
234    etype: i32,
235    kvno: Option<u32>,
236    cipher: &'a [u8],
237}
238
239/// Parsed PrincipalName (RFC 4120 §5.2.2).
240struct ParsedPrincipalName {
241    components: Vec<String>,
242}
243
244impl ParsedPrincipalName {
245    /// Reconstruct as "comp1/comp2/...".
246    fn to_string_without_realm(&self) -> String {
247        self.components.join("/")
248    }
249}
250
251/// Parsed Ticket fields (RFC 4120 §5.3).
252struct ParsedTicket<'a> {
253    realm: String,
254    sname: ParsedPrincipalName,
255    enc_part: ParsedEncryptedData<'a>,
256}
257
258/// Parsed AP-REQ (RFC 4120 §5.5.1).
259#[allow(dead_code)]
260struct ParsedApReq<'a> {
261    ap_options: u32,
262    ticket: ParsedTicket<'a>,
263    authenticator: ParsedEncryptedData<'a>,
264}
265
266/// Decrypted EncTicketPart (RFC 4120 §5.3).
267#[allow(dead_code)]
268struct DecryptedTicketPart {
269    flags_raw: Vec<u8>,
270    session_key_type: i32,
271    session_key_value: Vec<u8>,
272    crealm: String,
273    cname: ParsedPrincipalName,
274    auth_time: u64,
275    end_time: u64,
276    start_time: Option<u64>,
277    renew_till: Option<u64>,
278}
279
280/// Decrypted Authenticator (RFC 4120 §5.5.1).
281struct DecryptedAuthenticator {
282    crealm: String,
283    cname: ParsedPrincipalName,
284    cusec: u32,
285    ctime: u64,
286}
287
288fn parse_encrypted_data<'a>(data: &'a [u8]) -> Result<ParsedEncryptedData<'a>> {
289    let (seq, _) = parse_der(data)?;
290    if seq.tag_num != 16 {
291        return Err(AuthError::validation("EncryptedData: expected SEQUENCE"));
292    }
293    let fields = parse_der_contents(seq.value)?;
294
295    let etype_f = get_ctx_field(&fields, 0)
296        .ok_or_else(|| AuthError::validation("EncryptedData missing etype"))?;
297    let etype = parse_der_integer(unwrap_explicit(etype_f)?.value)? as i32;
298
299    let kvno = if let Some(f) = get_ctx_field(&fields, 1) {
300        Some(parse_der_integer(unwrap_explicit(f)?.value)? as u32)
301    } else {
302        None
303    };
304
305    let cipher_f = get_ctx_field(&fields, 2)
306        .ok_or_else(|| AuthError::validation("EncryptedData missing cipher"))?;
307    let cipher_tlv = unwrap_explicit(cipher_f)?;
308
309    Ok(ParsedEncryptedData {
310        etype,
311        kvno,
312        cipher: cipher_tlv.value,
313    })
314}
315
316fn parse_principal_name(data: &[u8]) -> Result<ParsedPrincipalName> {
317    let (seq, _) = parse_der(data)?;
318    let fields = parse_der_contents(seq.value)?;
319
320    // [1] SEQUENCE OF KerberosString
321    let strings_f = get_ctx_field(&fields, 1)
322        .ok_or_else(|| AuthError::validation("PrincipalName missing name-string"))?;
323    let (strings_seq, _) = parse_der(strings_f.value)?;
324    let string_tlvs = parse_der_contents(strings_seq.value)?;
325
326    let mut components = Vec::new();
327    for tlv in &string_tlvs {
328        components.push(parse_der_string(tlv.value)?);
329    }
330
331    Ok(ParsedPrincipalName { components })
332}
333
334fn parse_ticket<'a>(data: &'a [u8]) -> Result<ParsedTicket<'a>> {
335    // [APPLICATION 1] SEQUENCE
336    let (app, _) = parse_der(data)?;
337    if app.class != 1 || app.tag_num != 1 {
338        return Err(AuthError::validation("Expected Ticket (APPLICATION 1)"));
339    }
340    let (seq, _) = parse_der(app.value)?;
341    let fields = parse_der_contents(seq.value)?;
342
343    // [0] tkt-vno INTEGER
344    let vno_f =
345        get_ctx_field(&fields, 0).ok_or_else(|| AuthError::validation("Ticket missing tkt-vno"))?;
346    let vno = parse_der_integer(unwrap_explicit(vno_f)?.value)?;
347    if vno != 5 {
348        return Err(AuthError::validation(format!(
349            "Unsupported ticket version: {vno}"
350        )));
351    }
352
353    // [1] realm GeneralString
354    let realm_f =
355        get_ctx_field(&fields, 1).ok_or_else(|| AuthError::validation("Ticket missing realm"))?;
356    let realm = parse_der_string(unwrap_explicit(realm_f)?.value)?;
357
358    // [2] sname PrincipalName
359    let sname_f =
360        get_ctx_field(&fields, 2).ok_or_else(|| AuthError::validation("Ticket missing sname"))?;
361    let sname = parse_principal_name(sname_f.value)?;
362
363    // [3] enc-part EncryptedData
364    let enc_f = get_ctx_field(&fields, 3)
365        .ok_or_else(|| AuthError::validation("Ticket missing enc-part"))?;
366    let enc_part = parse_encrypted_data(enc_f.value)?;
367
368    Ok(ParsedTicket {
369        realm,
370        sname,
371        enc_part,
372    })
373}
374
375fn parse_ap_req<'a>(data: &'a [u8]) -> Result<ParsedApReq<'a>> {
376    // [APPLICATION 14] SEQUENCE
377    let (app, _) = parse_der(data)?;
378    if app.class != 1 || app.tag_num != 14 {
379        return Err(AuthError::validation("Expected AP-REQ (APPLICATION 14)"));
380    }
381    let (seq, _) = parse_der(app.value)?;
382    let fields = parse_der_contents(seq.value)?;
383
384    // [0] pvno INTEGER (must be 5)
385    let pvno_f =
386        get_ctx_field(&fields, 0).ok_or_else(|| AuthError::validation("AP-REQ missing pvno"))?;
387    let pvno = parse_der_integer(unwrap_explicit(pvno_f)?.value)?;
388    if pvno != 5 {
389        return Err(AuthError::validation(format!(
390            "Unsupported Kerberos version: {pvno}"
391        )));
392    }
393
394    // [1] msg-type INTEGER (must be 14)
395    let mt_f = get_ctx_field(&fields, 1)
396        .ok_or_else(|| AuthError::validation("AP-REQ missing msg-type"))?;
397    let mt = parse_der_integer(unwrap_explicit(mt_f)?.value)?;
398    if mt != 14 {
399        return Err(AuthError::validation(format!(
400            "Expected AP-REQ msg-type 14, got {mt}"
401        )));
402    }
403
404    // [2] ap-options BIT STRING
405    let opts_f = get_ctx_field(&fields, 2)
406        .ok_or_else(|| AuthError::validation("AP-REQ missing ap-options"))?;
407    let opts_tlv = unwrap_explicit(opts_f)?;
408    let ap_options = parse_ap_options(&opts_tlv)?;
409
410    // [3] ticket Ticket
411    let ticket_f =
412        get_ctx_field(&fields, 3).ok_or_else(|| AuthError::validation("AP-REQ missing ticket"))?;
413    let ticket = parse_ticket(ticket_f.value)?;
414
415    // [4] authenticator EncryptedData
416    let auth_f = get_ctx_field(&fields, 4)
417        .ok_or_else(|| AuthError::validation("AP-REQ missing authenticator"))?;
418    let authenticator = parse_encrypted_data(auth_f.value)?;
419
420    Ok(ParsedApReq {
421        ap_options,
422        ticket,
423        authenticator,
424    })
425}
426
427/// Parse AP-REQ options BIT STRING into a u32 flags word.
428fn parse_ap_options(tlv: &DerTlv<'_>) -> Result<u32> {
429    if tlv.value.len() < 2 {
430        return Ok(0);
431    }
432    // First byte = unused bits count, rest = flag bytes
433    let mut flags: u32 = 0;
434    for (i, &b) in tlv.value[1..].iter().enumerate() {
435        if i >= 4 {
436            break;
437        }
438        flags |= (b as u32) << (24 - i * 8);
439    }
440    Ok(flags)
441}
442
443/// Parse decrypted EncTicketPart ([APPLICATION 3] SEQUENCE).
444fn parse_enc_ticket_part(data: &[u8]) -> Result<DecryptedTicketPart> {
445    let (app, _) = parse_der(data)?;
446    if app.class != 1 || app.tag_num != 3 {
447        return Err(AuthError::validation(
448            "Expected EncTicketPart (APPLICATION 3)",
449        ));
450    }
451    let (seq, _) = parse_der(app.value)?;
452    let fields = parse_der_contents(seq.value)?;
453
454    // [0] flags BIT STRING
455    let flags_f = get_ctx_field(&fields, 0)
456        .ok_or_else(|| AuthError::validation("EncTicketPart missing flags"))?;
457    let flags_tlv = unwrap_explicit(flags_f)?;
458    let flags_raw = flags_tlv.value.to_vec();
459
460    // [1] key EncryptionKey
461    let key_f = get_ctx_field(&fields, 1)
462        .ok_or_else(|| AuthError::validation("EncTicketPart missing key"))?;
463    let (key_seq, _) = parse_der(key_f.value)?;
464    let key_fields = parse_der_contents(key_seq.value)?;
465    let key_type_f = get_ctx_field(&key_fields, 0)
466        .ok_or_else(|| AuthError::validation("EncryptionKey missing keytype"))?;
467    let key_type = parse_der_integer(unwrap_explicit(key_type_f)?.value)? as i32;
468    let key_val_f = get_ctx_field(&key_fields, 1)
469        .ok_or_else(|| AuthError::validation("EncryptionKey missing keyvalue"))?;
470    let key_value = unwrap_explicit(key_val_f)?.value.to_vec();
471
472    // [2] crealm GeneralString
473    let crealm_f = get_ctx_field(&fields, 2)
474        .ok_or_else(|| AuthError::validation("EncTicketPart missing crealm"))?;
475    let crealm = parse_der_string(unwrap_explicit(crealm_f)?.value)?;
476
477    // [3] cname PrincipalName
478    let cname_f = get_ctx_field(&fields, 3)
479        .ok_or_else(|| AuthError::validation("EncTicketPart missing cname"))?;
480    let cname = parse_principal_name(cname_f.value)?;
481
482    // [5] authtime KerberosTime
483    let authtime_f = get_ctx_field(&fields, 5)
484        .ok_or_else(|| AuthError::validation("EncTicketPart missing authtime"))?;
485    let auth_time = parse_kerberos_time(unwrap_explicit(authtime_f)?.value)?;
486
487    // [6] starttime KerberosTime OPTIONAL
488    let start_time = if let Some(f) = get_ctx_field(&fields, 6) {
489        Some(parse_kerberos_time(unwrap_explicit(f)?.value)?)
490    } else {
491        None
492    };
493
494    // [7] endtime KerberosTime
495    let endtime_f = get_ctx_field(&fields, 7)
496        .ok_or_else(|| AuthError::validation("EncTicketPart missing endtime"))?;
497    let end_time = parse_kerberos_time(unwrap_explicit(endtime_f)?.value)?;
498
499    // [8] renew-till KerberosTime OPTIONAL
500    let renew_till = if let Some(f) = get_ctx_field(&fields, 8) {
501        Some(parse_kerberos_time(unwrap_explicit(f)?.value)?)
502    } else {
503        None
504    };
505
506    Ok(DecryptedTicketPart {
507        flags_raw,
508        session_key_type: key_type,
509        session_key_value: key_value,
510        crealm,
511        cname,
512        auth_time,
513        end_time,
514        start_time,
515        renew_till,
516    })
517}
518
519/// Parse decrypted Authenticator ([APPLICATION 2] SEQUENCE).
520fn parse_authenticator(data: &[u8]) -> Result<DecryptedAuthenticator> {
521    let (app, _) = parse_der(data)?;
522    if app.class != 1 || app.tag_num != 2 {
523        return Err(AuthError::validation(
524            "Expected Authenticator (APPLICATION 2)",
525        ));
526    }
527    let (seq, _) = parse_der(app.value)?;
528    let fields = parse_der_contents(seq.value)?;
529
530    // [0] authenticator-vno INTEGER (5)
531    let vno_f = get_ctx_field(&fields, 0)
532        .ok_or_else(|| AuthError::validation("Authenticator missing vno"))?;
533    let vno = parse_der_integer(unwrap_explicit(vno_f)?.value)?;
534    if vno != 5 {
535        return Err(AuthError::validation(format!(
536            "Unsupported authenticator version: {vno}"
537        )));
538    }
539
540    // [1] crealm GeneralString
541    let crealm_f = get_ctx_field(&fields, 1)
542        .ok_or_else(|| AuthError::validation("Authenticator missing crealm"))?;
543    let crealm = parse_der_string(unwrap_explicit(crealm_f)?.value)?;
544
545    // [2] cname PrincipalName
546    let cname_f = get_ctx_field(&fields, 2)
547        .ok_or_else(|| AuthError::validation("Authenticator missing cname"))?;
548    let cname = parse_principal_name(cname_f.value)?;
549
550    // [3] cusec Microseconds (INTEGER)
551    let cusec_f = get_ctx_field(&fields, 3)
552        .ok_or_else(|| AuthError::validation("Authenticator missing cusec"))?;
553    let cusec = parse_der_integer(unwrap_explicit(cusec_f)?.value)? as u32;
554
555    // [4] ctime KerberosTime
556    let ctime_f = get_ctx_field(&fields, 4)
557        .ok_or_else(|| AuthError::validation("Authenticator missing ctime"))?;
558    let ctime = parse_kerberos_time(unwrap_explicit(ctime_f)?.value)?;
559
560    Ok(DecryptedAuthenticator {
561        crealm,
562        cname,
563        cusec,
564        ctime,
565    })
566}
567
568// ─── Kerberos Cryptography ───────────────────────────────────────────────────
569
570/// Greatest common divisor.
571fn gcd(a: usize, b: usize) -> usize {
572    if b == 0 { a } else { gcd(b, a % b) }
573}
574
575/// Least common multiple.
576fn lcm(a: usize, b: usize) -> usize {
577    a / gcd(a, b) * b
578}
579
580/// n-fold algorithm (RFC 3961 §5.1).
581///
582/// Folds an arbitrary-length input to an `output_len`-byte output.
583/// Direct port of the MIT Kerberos reference implementation.
584fn nfold(input: &[u8], output_len: usize) -> Vec<u8> {
585    let in_bytes = input.len();
586    let out_bytes = output_len;
587    let lcm_val = lcm(in_bytes, out_bytes);
588    let in_bits = in_bytes * 8;
589
590    let mut out = vec![0u8; out_bytes];
591    let mut byte: u32 = 0;
592
593    for i in (0..lcm_val).rev() {
594        // Compute the msbit in the input which gets added into this byte
595        let msbit =
596            ((in_bits - 1) + ((in_bits + 13) * (i / in_bytes)) + ((in_bytes - i % in_bytes) * 8))
597                % in_bits;
598
599        // Pull out the byte value from the input at the computed bit position
600        let high = input[((in_bytes - 1).wrapping_sub(msbit >> 3)) % in_bytes] as u32;
601        let low = input[(in_bytes.wrapping_sub(msbit >> 3)) % in_bytes] as u32;
602        byte += ((high << 8 | low) >> ((msbit & 7) + 1)) & 0xff;
603
604        // Do the addition
605        byte += out[i % out_bytes] as u32;
606        out[i % out_bytes] = (byte & 0xff) as u8;
607
608        // Keep around the carry bit
609        byte >>= 8;
610    }
611
612    // If there's a carry bit left over, add it back in
613    if byte != 0 {
614        for i in (0..out_bytes).rev() {
615            byte += out[i] as u32;
616            out[i] = (byte & 0xff) as u8;
617            byte >>= 8;
618        }
619    }
620
621    out
622}
623
624/// AES-ECB encrypt a single 16-byte block. Supports 16-byte (AES-128) or
625/// 32-byte (AES-256) keys.
626fn aes_ecb_encrypt(key: &[u8], block: &[u8; AES_BLOCK]) -> [u8; AES_BLOCK] {
627    let mut blk = aes::cipher::generic_array::GenericArray::clone_from_slice(block);
628    match key.len() {
629        16 => {
630            let cipher =
631                aes::Aes128::new(aes::cipher::generic_array::GenericArray::from_slice(key));
632            cipher.encrypt_block(&mut blk);
633        }
634        32 => {
635            let cipher =
636                aes::Aes256::new(aes::cipher::generic_array::GenericArray::from_slice(key));
637            cipher.encrypt_block(&mut blk);
638        }
639        _ => unreachable!("aes_ecb_encrypt: unsupported key length"),
640    }
641    let mut out = [0u8; AES_BLOCK];
642    out.copy_from_slice(&blk);
643    out
644}
645
646/// AES-ECB decrypt a single 16-byte block.
647fn aes_ecb_decrypt(key: &[u8], block: &[u8; AES_BLOCK]) -> [u8; AES_BLOCK] {
648    let mut blk = aes::cipher::generic_array::GenericArray::clone_from_slice(block);
649    match key.len() {
650        16 => {
651            let cipher =
652                aes::Aes128::new(aes::cipher::generic_array::GenericArray::from_slice(key));
653            cipher.decrypt_block(&mut blk);
654        }
655        32 => {
656            let cipher =
657                aes::Aes256::new(aes::cipher::generic_array::GenericArray::from_slice(key));
658            cipher.decrypt_block(&mut blk);
659        }
660        _ => unreachable!("aes_ecb_decrypt: unsupported key length"),
661    }
662    let mut out = [0u8; AES_BLOCK];
663    out.copy_from_slice(&blk);
664    out
665}
666
667/// XOR two byte slices of the same length.
668fn xor_bytes(a: &[u8], b: &[u8]) -> Vec<u8> {
669    a.iter().zip(b.iter()).map(|(&x, &y)| x ^ y).collect()
670}
671
672/// Derive a Kerberos sub-key from a base key using the RFC 3961 DK function.
673///
674/// `key_type` selects the derivative:
675///   - `0xAA` → Ke (encryption key)
676///   - `0x55` → Ki (integrity / HMAC key)
677///   - `0x99` → Kc (checksum key)
678fn derive_key_aes(base_key: &[u8], usage: u32, key_type: u8) -> Vec<u8> {
679    let constant = [
680        (usage >> 24) as u8,
681        (usage >> 16) as u8,
682        (usage >> 8) as u8,
683        usage as u8,
684        key_type,
685    ];
686
687    // n-fold the constant to the AES block size
688    let nfolded = nfold(&constant, AES_BLOCK);
689
690    let key_size = base_key.len(); // 16 for AES-128, 32 for AES-256
691    let mut derived = Vec::with_capacity(key_size);
692
693    let mut input: [u8; AES_BLOCK] = nfolded.try_into().expect("nfold produced wrong size");
694    while derived.len() < key_size {
695        let encrypted = aes_ecb_encrypt(base_key, &input);
696        derived.extend_from_slice(&encrypted);
697        input = encrypted;
698    }
699
700    derived.truncate(key_size);
701    derived
702}
703
704/// AES-CBC decrypt with a zero IV. Returns the full plaintext (same length as
705/// ciphertext, which must be a multiple of 16).
706fn aes_cbc_decrypt(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
707    if ciphertext.len() % AES_BLOCK != 0 || ciphertext.is_empty() {
708        return Err(AuthError::crypto(
709            "AES-CBC ciphertext must be a non-empty multiple of block size",
710        ));
711    }
712
713    let mut plaintext = Vec::with_capacity(ciphertext.len());
714    let mut prev = [0u8; AES_BLOCK]; // IV = all zeros for Kerberos
715
716    for chunk in ciphertext.chunks_exact(AES_BLOCK) {
717        let ct_block: [u8; AES_BLOCK] = chunk.try_into().unwrap();
718        let decrypted = aes_ecb_decrypt(key, &ct_block);
719        let pt_block = xor_bytes(&decrypted, &prev);
720        plaintext.extend_from_slice(&pt_block);
721        prev = ct_block;
722    }
723
724    Ok(plaintext)
725}
726
727/// AES-CTS (Cipher Text Stealing) decryption (RFC 3962 / RFC 3961).
728///
729/// Handles three cases:
730///   - Exactly 1 block: ECB decrypt with zero IV
731///   - Multiple of block size: standard CBC decrypt
732///   - Non-multiple: CBC-CTS with last-two-block swap
733fn aes_cts_decrypt(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
734    let n = ciphertext.len();
735    if n < AES_BLOCK {
736        return Err(AuthError::crypto("AES-CTS ciphertext too short"));
737    }
738
739    if n == AES_BLOCK {
740        // Single block: ECB decrypt XOR with zero IV = just ECB decrypt
741        let ct: [u8; AES_BLOCK] = ciphertext.try_into().unwrap();
742        return Ok(aes_ecb_decrypt(key, &ct).to_vec());
743    }
744
745    if n % AES_BLOCK == 0 {
746        // Exact multiple: standard CBC
747        return aes_cbc_decrypt(key, ciphertext);
748    }
749
750    // CTS case: non-multiple of block size
751    let partial_len = n % AES_BLOCK;
752    let num_full_blocks = n / AES_BLOCK; // at least 1
753    let preceding_len = (num_full_blocks - 1) * AES_BLOCK;
754
755    let c_second_last: [u8; AES_BLOCK] = ciphertext[preceding_len..preceding_len + AES_BLOCK]
756        .try_into()
757        .unwrap();
758    let c_last = &ciphertext[preceding_len + AES_BLOCK..];
759
760    // Determine the CBC IV for the CTS pair
761    let prev_cipher = if preceding_len >= AES_BLOCK {
762        &ciphertext[preceding_len - AES_BLOCK..preceding_len]
763    } else {
764        &[0u8; AES_BLOCK][..] // zero IV
765    };
766
767    // ECB decrypt the second-to-last ciphertext block
768    let d = aes_ecb_decrypt(key, &c_second_last);
769
770    // Recover the partial plaintext (last block)
771    let p_last = xor_bytes(&d[..partial_len], c_last);
772
773    // Recover the full "un-swapped" penultimate ciphertext block
774    let mut c_recovered = [0u8; AES_BLOCK];
775    c_recovered[..partial_len].copy_from_slice(c_last);
776    c_recovered[partial_len..].copy_from_slice(&d[partial_len..]);
777
778    // CBC decrypt the recovered block
779    let decrypted_recovered = aes_ecb_decrypt(key, &c_recovered);
780    let p_second_last = xor_bytes(&decrypted_recovered, prev_cipher);
781
782    // CBC decrypt the preceding blocks (if any)
783    let mut plaintext = if preceding_len > 0 {
784        aes_cbc_decrypt(key, &ciphertext[..preceding_len])?
785    } else {
786        Vec::new()
787    };
788
789    plaintext.extend_from_slice(&p_second_last);
790    plaintext.extend_from_slice(&p_last);
791
792    Ok(plaintext)
793}
794
795/// Compute HMAC-SHA1 over the given data, returning the full 20-byte output.
796fn hmac_sha1(key: &[u8], data: &[u8]) -> Vec<u8> {
797    use hmac::{Hmac, Mac};
798    type HmacSha1 = Hmac<sha1::Sha1>;
799
800    let mut mac = <HmacSha1 as Mac>::new_from_slice(key).expect("HMAC-SHA1 accepts any key length");
801    mac.update(data);
802    mac.finalize().into_bytes().to_vec()
803}
804
805/// Decrypt a Kerberos AES-CTS-HMAC-SHA1-96 ciphertext (RFC 3962).
806///
807/// Layout: `ciphertext_body || hmac_sha1_96` where
808///   - `ciphertext_body` = AES-CTS encrypted `confounder || plaintext`
809///   - `hmac_sha1_96`    = HMAC-SHA1 truncated to 96 bits (12 bytes)
810fn decrypt_aes_cts(base_key: &[u8], ciphertext: &[u8], etype: i32, usage: u32) -> Result<Vec<u8>> {
811    let expected_key_len = match etype {
812        ETYPE_AES128 => 16,
813        ETYPE_AES256 => 32,
814        _ => {
815            return Err(AuthError::crypto(format!(
816                "Unsupported encryption type {etype}; only AES etypes 17/18 are supported"
817            )));
818        }
819    };
820
821    if base_key.len() != expected_key_len {
822        return Err(AuthError::crypto(format!(
823            "Key length mismatch: expected {expected_key_len}, got {}",
824            base_key.len()
825        )));
826    }
827
828    if ciphertext.len() < CONFOUNDER_LEN + HMAC_LEN {
829        return Err(AuthError::crypto(
830            "Ciphertext too short for AES-CTS envelope",
831        ));
832    }
833
834    let ct_body = &ciphertext[..ciphertext.len() - HMAC_LEN];
835    let expected_hmac = &ciphertext[ciphertext.len() - HMAC_LEN..];
836
837    // Derive Ke (encryption) and Ki (integrity) sub-keys
838    let ke = derive_key_aes(base_key, usage, 0xAA);
839    let ki = derive_key_aes(base_key, usage, 0x55);
840
841    // Decrypt
842    let plaintext_with_confounder = aes_cts_decrypt(&ke, ct_body)?;
843
844    // Verify HMAC-SHA1-96 over the decrypted plaintext (including confounder)
845    let computed_hmac = hmac_sha1(&ki, &plaintext_with_confounder);
846    if computed_hmac[..HMAC_LEN].ct_eq(expected_hmac).unwrap_u8() != 1 {
847        return Err(AuthError::crypto(
848            "Kerberos HMAC verification failed — wrong key or corrupted ticket",
849        ));
850    }
851
852    // Strip confounder
853    Ok(plaintext_with_confounder[CONFOUNDER_LEN..].to_vec())
854}
855
856// ─── SPNEGO Parsing ──────────────────────────────────────────────────────────
857
858/// Parse a GSS-API / SPNEGO initial context token.
859///
860/// Structure:
861/// ```text
862/// [APPLICATION 0] SEQUENCE {
863///     thisMech  OID (SPNEGO),
864///     innerToken  NegTokenInit (as [CONTEXT 0])
865/// }
866/// ```
867fn parse_spnego_init_token(data: &[u8]) -> Result<SpnegoToken> {
868    // [APPLICATION 0] wrapper
869    let (app, _) = parse_der(data)?;
870    if app.class != 1 || app.tag_num != 0 {
871        return Err(AuthError::validation(
872            "Expected GSS-API InitialContextToken (APPLICATION 0)",
873        ));
874    }
875
876    // Inner: OID followed by NegTokenInit
877    let (oid_tlv, rest) = parse_der(app.value)?;
878    if !oid_matches(&oid_tlv, SPNEGO_OID_BYTES) {
879        return Err(AuthError::validation("Not an SPNEGO token"));
880    }
881
882    // NegTokenInit is [CONTEXT 0] IMPLICIT SEQUENCE
883    let (neg_init_wrapper, _) = parse_der(rest)?;
884    if neg_init_wrapper.class != 2 || neg_init_wrapper.tag_num != 0 {
885        return Err(AuthError::validation("Expected NegTokenInit ([CONTEXT 0])"));
886    }
887
888    // Inner SEQUENCE
889    let (neg_init_seq, _) = parse_der(neg_init_wrapper.value)?;
890    let fields = parse_der_contents(neg_init_seq.value)?;
891
892    // [0] mechTypes: SEQUENCE OF OID
893    let mech_types_f = get_ctx_field(&fields, 0)
894        .ok_or_else(|| AuthError::validation("NegTokenInit missing mechTypes"))?;
895    let (mech_seq, _) = parse_der(mech_types_f.value)?;
896    let mech_oids = parse_der_contents(mech_seq.value)?;
897
898    // Check if Kerberos 5 is among the offered mechanisms
899    let has_krb5 = mech_oids.iter().any(|o| oid_matches(o, KRB5_OID_BYTES));
900    if !has_krb5 {
901        return Err(AuthError::validation(
902            "SPNEGO NegTokenInit does not offer Kerberos 5",
903        ));
904    }
905
906    // [2] mechToken: OCTET STRING (the AP-REQ)
907    let mech_token_f = get_ctx_field(&fields, 2)
908        .ok_or_else(|| AuthError::validation("NegTokenInit missing mechToken"))?;
909    let mech_token_tlv = unwrap_explicit(mech_token_f)?;
910
911    Ok(SpnegoToken {
912        mech_oid: oid::KERBEROS_V5.to_string(),
913        mech_token: mech_token_tlv.value.to_vec(),
914        state: SpnegoState::Initial,
915    })
916}
917
918/// Parse a SPNEGO NegTokenResp ([CONTEXT 1]).
919fn parse_spnego_resp_token(data: &[u8]) -> Result<SpnegoToken> {
920    let (wrapper, _) = parse_der(data)?;
921    if wrapper.class != 2 || wrapper.tag_num != 1 {
922        return Err(AuthError::validation("Expected NegTokenResp ([CONTEXT 1])"));
923    }
924
925    let (seq, _) = parse_der(wrapper.value)?;
926    let fields = parse_der_contents(seq.value)?;
927
928    // [2] responseToken OCTET STRING OPTIONAL
929    let mech_token = if let Some(f) = get_ctx_field(&fields, 2) {
930        unwrap_explicit(f)?.value.to_vec()
931    } else {
932        Vec::new()
933    };
934
935    Ok(SpnegoToken {
936        mech_oid: oid::KERBEROS_V5.to_string(),
937        mech_token,
938        state: SpnegoState::Continue,
939    })
940}
941
942// ─── Configuration ───────────────────────────────────────────────────────────
943
944/// Kerberos / SPNEGO configuration.
945#[derive(Debug, Clone)]
946pub struct KerberosConfig {
947    /// Service principal name (e.g. `HTTP/server.example.com@REALM`).
948    pub service_principal: String,
949
950    /// Kerberos realm (e.g. `EXAMPLE.COM`).
951    pub realm: String,
952
953    /// Path to the keytab file.
954    pub keytab_path: Option<String>,
955
956    /// KDC addresses for ticket verification.
957    pub kdc_addresses: Vec<String>,
958
959    /// Maximum allowed clock skew (default: 300 seconds / 5 minutes).
960    pub max_clock_skew_secs: u64,
961
962    /// Whether to allow delegation (forwarded tickets).
963    pub allow_delegation: bool,
964
965    /// Maximum replay cache entries before eviction.
966    pub replay_cache_max_entries: usize,
967}
968
969impl Default for KerberosConfig {
970    fn default() -> Self {
971        Self {
972            service_principal: String::new(),
973            realm: String::new(),
974            keytab_path: None,
975            kdc_addresses: Vec::new(),
976            max_clock_skew_secs: 300,
977            allow_delegation: false,
978            replay_cache_max_entries: 100_000,
979        }
980    }
981}
982
983impl KerberosConfig {
984    /// Start building a [`KerberosConfig`] with the two required fields.
985    ///
986    /// All optional fields default to [`KerberosConfig::default()`] values.
987    /// Call [`.build()`](KerberosConfigBuilder::build) to validate and obtain
988    /// the finished config.
989    ///
990    /// # Example
991    /// ```rust,ignore
992    /// use auth_framework::protocols::kerberos::KerberosConfig;
993    ///
994    /// let config = KerberosConfig::builder(
995    ///         "HTTP/server.example.com@EXAMPLE.COM",
996    ///         "EXAMPLE.COM",
997    ///     )
998    ///     .keytab_path("/etc/krb5.keytab")
999    ///     .add_kdc("kdc1.example.com:88")
1000    ///     .add_kdc("kdc2.example.com:88")
1001    ///     .build();
1002    /// ```
1003    pub fn builder(
1004        service_principal: impl Into<String>,
1005        realm: impl Into<String>,
1006    ) -> KerberosConfigBuilder {
1007        KerberosConfigBuilder {
1008            config: KerberosConfig {
1009                service_principal: service_principal.into(),
1010                realm: realm.into(),
1011                ..Default::default()
1012            },
1013        }
1014    }
1015
1016    /// Shorthand for an Active Directory environment.
1017    ///
1018    /// Sets `allow_delegation` to `true` (common in AD) and leaves the
1019    /// rest at defaults.
1020    ///
1021    /// # Example
1022    /// ```rust,ignore
1023    /// let config = KerberosConfig::active_directory(
1024    ///     "HTTP/server.corp.example.com@CORP.EXAMPLE.COM",
1025    ///     "CORP.EXAMPLE.COM",
1026    /// );
1027    /// ```
1028    pub fn active_directory(
1029        service_principal: impl Into<String>,
1030        realm: impl Into<String>,
1031    ) -> Self {
1032        Self {
1033            service_principal: service_principal.into(),
1034            realm: realm.into(),
1035            allow_delegation: true,
1036            ..Default::default()
1037        }
1038    }
1039}
1040
1041/// Builder for [`KerberosConfig`].
1042///
1043/// Created via [`KerberosConfig::builder()`].
1044pub struct KerberosConfigBuilder {
1045    config: KerberosConfig,
1046}
1047
1048impl KerberosConfigBuilder {
1049    /// Set the path to the keytab file.
1050    pub fn keytab_path(mut self, path: impl Into<String>) -> Self {
1051        self.config.keytab_path = Some(path.into());
1052        self
1053    }
1054
1055    /// Append a KDC address (e.g. `"kdc.example.com:88"`).
1056    pub fn add_kdc(mut self, addr: impl Into<String>) -> Self {
1057        self.config.kdc_addresses.push(addr.into());
1058        self
1059    }
1060
1061    /// Set the maximum allowed clock skew in seconds (default: 300).
1062    pub fn max_clock_skew_secs(mut self, secs: u64) -> Self {
1063        self.config.max_clock_skew_secs = secs;
1064        self
1065    }
1066
1067    /// Enable or disable ticket delegation (default: `false`).
1068    pub fn allow_delegation(mut self, allow: bool) -> Self {
1069        self.config.allow_delegation = allow;
1070        self
1071    }
1072
1073    /// Set the maximum replay-cache size (default: 100 000).
1074    pub fn replay_cache_max_entries(mut self, max: usize) -> Self {
1075        self.config.replay_cache_max_entries = max;
1076        self
1077    }
1078
1079    /// Consume the builder and return the finished [`KerberosConfig`].
1080    pub fn build(self) -> KerberosConfig {
1081        self.config
1082    }
1083}
1084
1085// ─── Data Types ──────────────────────────────────────────────────────────────
1086
1087/// Kerberos authentication token OID constants.
1088pub mod oid {
1089    /// SPNEGO OID: 1.3.6.1.5.5.2
1090    pub const SPNEGO: &str = "1.3.6.1.5.5.2";
1091    /// Kerberos 5 OID: 1.2.840.113554.1.2.2
1092    pub const KERBEROS_V5: &str = "1.2.840.113554.1.2.2";
1093}
1094
1095/// Result of Kerberos authentication.
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1097pub struct KerberosAuthResult {
1098    /// Authenticated client principal (e.g. `user@REALM`).
1099    pub client_principal: String,
1100
1101    /// Kerberos realm.
1102    pub realm: String,
1103
1104    /// When the ticket was issued.
1105    pub auth_time: u64,
1106
1107    /// When the ticket expires.
1108    pub end_time: u64,
1109
1110    /// Whether this is a delegated (forwarded) ticket.
1111    pub is_delegated: bool,
1112
1113    /// Session flags extracted from the ticket.
1114    pub flags: KerberosTicketFlags,
1115
1116    /// SPNEGO response token to return to the client (mutual auth).
1117    pub response_token: Option<String>,
1118}
1119
1120/// Kerberos ticket flags (RFC 4120 §5.3).
1121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1122pub struct KerberosTicketFlags {
1123    pub forwardable: bool,
1124    pub forwarded: bool,
1125    pub proxiable: bool,
1126    pub proxy: bool,
1127    pub may_postdate: bool,
1128    pub postdated: bool,
1129    pub renewable: bool,
1130    pub pre_authent: bool,
1131    pub hw_authent: bool,
1132}
1133
1134/// SPNEGO negotiation state.
1135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1136pub enum SpnegoState {
1137    /// Initial negotiation — expecting NegTokenInit.
1138    Initial,
1139    /// Continuation — expecting NegTokenResp.
1140    Continue,
1141    /// Negotiation complete.
1142    Completed,
1143    /// Negotiation rejected.
1144    Rejected,
1145}
1146
1147/// Parsed SPNEGO token (simplified).
1148#[derive(Debug, Clone)]
1149pub struct SpnegoToken {
1150    /// Which mechanism was selected.
1151    pub mech_oid: String,
1152    /// The mechanism-specific token bytes (AP-REQ for Kerberos).
1153    pub mech_token: Vec<u8>,
1154    /// Negotiation state.
1155    pub state: SpnegoState,
1156}
1157
1158/// Kerberos keytab entry.
1159#[derive(Debug, Clone)]
1160pub struct KeytabEntry {
1161    pub principal: String,
1162    pub realm: String,
1163    pub kvno: u32,
1164    pub key_type: u32,
1165    pub key_data: Vec<u8>,
1166}
1167
1168// ─── Manager ─────────────────────────────────────────────────────────────────
1169
1170/// Kerberos / SPNEGO authentication manager.
1171#[derive(Debug)]
1172pub struct KerberosManager {
1173    config: KerberosConfig,
1174    /// Replay cache: authenticator hash → timestamp.
1175    replay_cache: Arc<RwLock<HashMap<Vec<u8>, u64>>>,
1176    /// Loaded keytab entries.
1177    keytab_entries: Arc<RwLock<Vec<KeytabEntry>>>,
1178}
1179
1180impl KerberosManager {
1181    /// Create a new Kerberos manager.
1182    pub fn new(config: KerberosConfig) -> Result<Self> {
1183        if config.service_principal.is_empty() {
1184            return Err(AuthError::config("Kerberos service principal must be set"));
1185        }
1186        if config.realm.is_empty() {
1187            return Err(AuthError::config("Kerberos realm must be set"));
1188        }
1189
1190        Ok(Self {
1191            config,
1192            replay_cache: Arc::new(RwLock::new(HashMap::new())),
1193            keytab_entries: Arc::new(RwLock::new(Vec::new())),
1194        })
1195    }
1196
1197    /// Load keytab entries from file.
1198    pub async fn load_keytab(&self, path: &str) -> Result<usize> {
1199        let data = tokio::fs::read(path)
1200            .await
1201            .map_err(|e| AuthError::config(format!("Failed to read keytab file: {e}")))?;
1202
1203        let entries = parse_keytab(&data)?;
1204        let count = entries.len();
1205
1206        let mut kt = self.keytab_entries.write().await;
1207        *kt = entries;
1208
1209        Ok(count)
1210    }
1211
1212    /// Process an HTTP `Authorization: Negotiate <token>` header.
1213    ///
1214    /// Returns the authentication result on success, or an appropriate error.
1215    /// The caller should return the `response_token` (if any) in a
1216    /// `WWW-Authenticate: Negotiate <token>` response header.
1217    pub async fn authenticate(&self, negotiate_token: &str) -> Result<KerberosAuthResult> {
1218        let token_bytes = base64::engine::general_purpose::STANDARD
1219            .decode(negotiate_token.trim())
1220            .map_err(|e| AuthError::validation(format!("Invalid Negotiate token encoding: {e}")))?;
1221
1222        if token_bytes.is_empty() {
1223            return Err(AuthError::validation("Empty Negotiate token"));
1224        }
1225
1226        // Parse SPNEGO wrapper
1227        let spnego = self.parse_spnego_token(&token_bytes)?;
1228
1229        if spnego.mech_oid != oid::KERBEROS_V5 {
1230            return Err(AuthError::validation(format!(
1231                "Unsupported SPNEGO mechanism: {}",
1232                spnego.mech_oid
1233            )));
1234        }
1235
1236        // Validate the Kerberos AP-REQ
1237        let result = self.validate_ap_req(&spnego.mech_token).await?;
1238
1239        Ok(result)
1240    }
1241
1242    /// Generate a `WWW-Authenticate: Negotiate` challenge header value.
1243    pub fn generate_challenge(&self) -> String {
1244        "Negotiate".to_string()
1245    }
1246
1247    /// Parse an SPNEGO token wrapper using proper ASN.1 DER parsing.
1248    fn parse_spnego_token(&self, data: &[u8]) -> Result<SpnegoToken> {
1249        if data.len() < 2 {
1250            return Err(AuthError::validation("SPNEGO token too short"));
1251        }
1252
1253        match data[0] {
1254            // APPLICATION 0 CONSTRUCTED — GSS-API InitialContextToken
1255            0x60 => parse_spnego_init_token(data),
1256            // CONTEXT 1 — NegTokenResp
1257            0xa1 => parse_spnego_resp_token(data),
1258            // Assume raw Kerberos AP-REQ (no SPNEGO wrapper)
1259            _ => Ok(SpnegoToken {
1260                mech_oid: oid::KERBEROS_V5.to_string(),
1261                mech_token: data.to_vec(),
1262                state: SpnegoState::Initial,
1263            }),
1264        }
1265    }
1266
1267    /// Validate a Kerberos AP-REQ message.
1268    ///
1269    /// Performs full cryptographic validation:
1270    /// 1. Parses the AP-REQ ASN.1 structure
1271    /// 2. Looks up the service key from the keytab
1272    /// 3. Decrypts the ticket using AES-CTS-HMAC-SHA1-96
1273    /// 4. Decrypts the authenticator using the session key
1274    /// 5. Verifies timestamps and checks for replay attacks
1275    async fn validate_ap_req(&self, ap_req_bytes: &[u8]) -> Result<KerberosAuthResult> {
1276        let keytab = self.keytab_entries.read().await;
1277        if keytab.is_empty() {
1278            return Err(AuthError::config(
1279                "No keytab loaded — cannot validate Kerberos tickets",
1280            ));
1281        }
1282
1283        // ── Step 1: Parse AP-REQ ──
1284        let ap_req = parse_ap_req(ap_req_bytes)?;
1285
1286        // ── Step 2: Find matching keytab entry ──
1287        let ticket_etype = ap_req.ticket.enc_part.etype;
1288        let ticket_kvno = ap_req.ticket.enc_part.kvno;
1289        let ticket_sname = format!(
1290            "{}@{}",
1291            ap_req.ticket.sname.to_string_without_realm(),
1292            ap_req.ticket.realm
1293        );
1294
1295        let entry = keytab
1296            .iter()
1297            .find(|e| {
1298                e.principal == ticket_sname
1299                    && e.key_type == ticket_etype as u32
1300                    && ticket_kvno.is_none_or(|v| e.kvno == v)
1301            })
1302            .or_else(|| {
1303                // Fallback: match by realm and key type only
1304                keytab
1305                    .iter()
1306                    .find(|e| e.realm == ap_req.ticket.realm && e.key_type == ticket_etype as u32)
1307            })
1308            .ok_or_else(|| {
1309                AuthError::config(format!(
1310                    "No keytab entry for principal={ticket_sname} etype={ticket_etype}"
1311                ))
1312            })?;
1313
1314        // ── Step 3: Decrypt the ticket ──
1315        let ticket_plaintext = decrypt_aes_cts(
1316            &entry.key_data,
1317            ap_req.ticket.enc_part.cipher,
1318            ticket_etype,
1319            KEY_USAGE_TICKET,
1320        )?;
1321
1322        let ticket_part = parse_enc_ticket_part(&ticket_plaintext)?;
1323
1324        // ── Step 4: Check ticket expiration ──
1325        let now = SystemTime::now()
1326            .duration_since(UNIX_EPOCH)
1327            .map_err(|e| AuthError::internal(format!("Clock error: {e}")))?
1328            .as_secs();
1329
1330        if now > ticket_part.end_time + self.config.max_clock_skew_secs {
1331            return Err(AuthError::validation("Kerberos ticket has expired"));
1332        }
1333
1334        if let Some(start) = ticket_part.start_time {
1335            if now + self.config.max_clock_skew_secs < start {
1336                return Err(AuthError::validation("Kerberos ticket is not yet valid"));
1337            }
1338        }
1339
1340        // ── Step 5: Decrypt the authenticator using the session key ──
1341        let auth_etype = ap_req.authenticator.etype;
1342        let auth_plaintext = decrypt_aes_cts(
1343            &ticket_part.session_key_value,
1344            ap_req.authenticator.cipher,
1345            auth_etype,
1346            KEY_USAGE_AP_REQ_AUTH,
1347        )?;
1348
1349        let authenticator = parse_authenticator(&auth_plaintext)?;
1350
1351        // ── Step 6: Verify authenticator matches ticket ──
1352        if authenticator.crealm != ticket_part.crealm {
1353            return Err(AuthError::validation(
1354                "Authenticator crealm does not match ticket",
1355            ));
1356        }
1357
1358        if authenticator.cname.to_string_without_realm()
1359            != ticket_part.cname.to_string_without_realm()
1360        {
1361            return Err(AuthError::validation(
1362                "Authenticator cname does not match ticket",
1363            ));
1364        }
1365
1366        // ── Step 7: Check clock skew ──
1367        let time_diff = if now > authenticator.ctime {
1368            now - authenticator.ctime
1369        } else {
1370            authenticator.ctime - now
1371        };
1372
1373        if time_diff > self.config.max_clock_skew_secs {
1374            return Err(AuthError::validation(format!(
1375                "Authenticator clock skew too large: {time_diff}s (max {}s)",
1376                self.config.max_clock_skew_secs
1377            )));
1378        }
1379
1380        // ── Step 8: Replay detection (ctime + cusec + cname) ──
1381        self.check_replay_authenticator(
1382            authenticator.ctime,
1383            authenticator.cusec,
1384            &authenticator.cname.to_string_without_realm(),
1385        )
1386        .await?;
1387
1388        // ── Build result ──
1389        let client_principal = format!(
1390            "{}@{}",
1391            ticket_part.cname.to_string_without_realm(),
1392            ticket_part.crealm
1393        );
1394        let is_delegated = test_bit_flag(&ticket_part.flags_raw, 2); // bit 2 = forwarded
1395
1396        let flags = KerberosTicketFlags {
1397            forwardable: test_bit_flag(&ticket_part.flags_raw, 1),
1398            forwarded: test_bit_flag(&ticket_part.flags_raw, 2),
1399            proxiable: test_bit_flag(&ticket_part.flags_raw, 3),
1400            proxy: test_bit_flag(&ticket_part.flags_raw, 4),
1401            may_postdate: test_bit_flag(&ticket_part.flags_raw, 5),
1402            postdated: test_bit_flag(&ticket_part.flags_raw, 6),
1403            renewable: test_bit_flag(&ticket_part.flags_raw, 8),
1404            pre_authent: test_bit_flag(&ticket_part.flags_raw, 10),
1405            hw_authent: test_bit_flag(&ticket_part.flags_raw, 11),
1406        };
1407
1408        // Reject delegated tickets if not allowed
1409        if is_delegated && !self.config.allow_delegation {
1410            return Err(AuthError::validation(
1411                "Delegated (forwarded) tickets are not allowed by policy",
1412            ));
1413        }
1414
1415        Ok(KerberosAuthResult {
1416            client_principal,
1417            realm: ticket_part.crealm,
1418            auth_time: ticket_part.auth_time,
1419            end_time: ticket_part.end_time,
1420            is_delegated,
1421            flags,
1422            response_token: None,
1423        })
1424    }
1425
1426    /// Check for authenticator replay using (ctime, cusec, cname) tuple.
1427    async fn check_replay_authenticator(&self, ctime: u64, cusec: u32, cname: &str) -> Result<()> {
1428        // Build a unique key from the authenticator's identifying fields
1429        let mut hasher = sha2::Sha256::new();
1430        hasher.update(ctime.to_be_bytes());
1431        hasher.update(cusec.to_be_bytes());
1432        hasher.update(cname.as_bytes());
1433        let hash = hasher.finalize().to_vec();
1434
1435        let now = SystemTime::now()
1436            .duration_since(UNIX_EPOCH)
1437            .map_err(|e| AuthError::internal(format!("Clock error: {e}")))?
1438            .as_secs();
1439
1440        let mut cache = self.replay_cache.write().await;
1441
1442        // Evict stale entries
1443        let cutoff = now.saturating_sub(self.config.max_clock_skew_secs * 2);
1444        cache.retain(|_, &mut ts| ts > cutoff);
1445
1446        if cache.contains_key(&hash) {
1447            return Err(AuthError::validation("Kerberos replay attack detected"));
1448        }
1449
1450        if cache.len() >= self.config.replay_cache_max_entries {
1451            return Err(AuthError::internal("Kerberos replay cache full"));
1452        }
1453
1454        cache.insert(hash, now);
1455        Ok(())
1456    }
1457
1458    /// Check for replay attacks using a raw token hash (for backwards compat).
1459    pub async fn check_replay(&self, token_data: &[u8]) -> Result<()> {
1460        let hash = sha2::Sha256::digest(token_data).to_vec();
1461
1462        let now = SystemTime::now()
1463            .duration_since(UNIX_EPOCH)
1464            .map_err(|e| AuthError::internal(format!("Clock error: {e}")))?
1465            .as_secs();
1466
1467        let mut cache = self.replay_cache.write().await;
1468
1469        let cutoff = now.saturating_sub(self.config.max_clock_skew_secs * 2);
1470        cache.retain(|_, &mut ts| ts > cutoff);
1471
1472        if cache.contains_key(&hash) {
1473            return Err(AuthError::validation("Kerberos replay attack detected"));
1474        }
1475
1476        if cache.len() >= self.config.replay_cache_max_entries {
1477            return Err(AuthError::internal("Kerberos replay cache full"));
1478        }
1479
1480        cache.insert(hash, now);
1481        Ok(())
1482    }
1483
1484    /// Generate a mutual-authentication response token.
1485    #[allow(dead_code)]
1486    fn generate_response_token(&self) -> Result<Option<String>> {
1487        let rng = ring::rand::SystemRandom::new();
1488        let mut nonce = [0u8; 16];
1489        rng.fill(&mut nonce)
1490            .map_err(|_| AuthError::crypto("Failed to generate SPNEGO response nonce"))?;
1491
1492        let encoded = base64::engine::general_purpose::STANDARD.encode(nonce);
1493        Ok(Some(encoded))
1494    }
1495}
1496
1497// ─── Keytab Parsing ──────────────────────────────────────────────────────────
1498
1499/// Parse a Kerberos keytab file (MIT format).
1500///
1501/// Keytab format:
1502/// - 2 bytes: version (0x0502 for v2)
1503/// - Repeated entries: length-prefixed principal + key data
1504fn parse_keytab(data: &[u8]) -> Result<Vec<KeytabEntry>> {
1505    if data.len() < 4 {
1506        return Err(AuthError::config("Keytab file too short"));
1507    }
1508
1509    // Check magic number
1510    let version = u16::from_be_bytes([data[0], data[1]]);
1511    if version != 0x0502 && version != 0x0501 {
1512        return Err(AuthError::config(format!(
1513            "Unsupported keytab version: 0x{version:04x}"
1514        )));
1515    }
1516
1517    let mut entries = Vec::new();
1518    let mut pos = 2;
1519
1520    while pos + 4 <= data.len() {
1521        let entry_len =
1522            i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1523        pos += 4;
1524
1525        if entry_len <= 0 {
1526            // Deleted/empty entry — skip
1527            pos += entry_len.unsigned_abs() as usize;
1528            continue;
1529        }
1530        let entry_len = entry_len as usize;
1531
1532        if pos + entry_len > data.len() {
1533            break;
1534        }
1535
1536        let entry_data = &data[pos..pos + entry_len];
1537        if let Ok(entry) = parse_keytab_entry(entry_data, version) {
1538            entries.push(entry);
1539        }
1540
1541        pos += entry_len;
1542    }
1543
1544    Ok(entries)
1545}
1546
1547/// Parse a single keytab entry.
1548fn parse_keytab_entry(data: &[u8], _version: u16) -> Result<KeytabEntry> {
1549    if data.len() < 8 {
1550        return Err(AuthError::config("Keytab entry too short"));
1551    }
1552
1553    // Read number of principal components
1554    let num_components = u16::from_be_bytes([data[0], data[1]]) as usize;
1555    let mut pos = 2;
1556
1557    // Read realm
1558    if pos + 2 > data.len() {
1559        return Err(AuthError::config("Keytab entry truncated at realm length"));
1560    }
1561    let realm_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
1562    pos += 2;
1563
1564    if pos + realm_len > data.len() {
1565        return Err(AuthError::config("Keytab entry truncated at realm data"));
1566    }
1567    let realm = String::from_utf8_lossy(&data[pos..pos + realm_len]).to_string();
1568    pos += realm_len;
1569
1570    // Read principal components
1571    let mut principal_parts = Vec::new();
1572    for _ in 0..num_components {
1573        if pos + 2 > data.len() {
1574            return Err(AuthError::config("Keytab entry truncated at component"));
1575        }
1576        let comp_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
1577        pos += 2;
1578        if pos + comp_len > data.len() {
1579            return Err(AuthError::config(
1580                "Keytab entry truncated at component data",
1581            ));
1582        }
1583        principal_parts.push(String::from_utf8_lossy(&data[pos..pos + comp_len]).to_string());
1584        pos += comp_len;
1585    }
1586    let principal = format!("{}@{}", principal_parts.join("/"), realm);
1587
1588    // Skip name_type (4 bytes), timestamp (4 bytes)
1589    pos += 8;
1590    if pos >= data.len() {
1591        return Err(AuthError::config("Keytab entry truncated at kvno"));
1592    }
1593
1594    // Read kvno (1 byte in v1, may have 4-byte extension at end)
1595    let kvno = data.get(pos).copied().unwrap_or(0) as u32;
1596    pos += 1;
1597
1598    // Read key type
1599    if pos + 2 > data.len() {
1600        return Err(AuthError::config("Keytab entry truncated at key type"));
1601    }
1602    let key_type = u16::from_be_bytes([data[pos], data[pos + 1]]) as u32;
1603    pos += 2;
1604
1605    // Read key data
1606    if pos + 2 > data.len() {
1607        return Err(AuthError::config("Keytab entry truncated at key length"));
1608    }
1609    let key_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
1610    pos += 2;
1611
1612    if pos + key_len > data.len() {
1613        return Err(AuthError::config("Keytab entry truncated at key data"));
1614    }
1615    let key_data = data[pos..pos + key_len].to_vec();
1616
1617    Ok(KeytabEntry {
1618        principal,
1619        realm,
1620        kvno,
1621        key_type,
1622        key_data,
1623    })
1624}
1625
1626#[cfg(test)]
1627mod tests {
1628    use super::*;
1629
1630    #[test]
1631    fn test_config_defaults() {
1632        let config = KerberosConfig::default();
1633        assert_eq!(config.max_clock_skew_secs, 300);
1634        assert!(!config.allow_delegation);
1635    }
1636
1637    #[test]
1638    fn test_manager_requires_principal() {
1639        let config = KerberosConfig::default();
1640        let err = KerberosManager::new(config).unwrap_err();
1641        assert!(err.to_string().contains("service principal"));
1642    }
1643
1644    #[test]
1645    fn test_manager_requires_realm() {
1646        let config = KerberosConfig {
1647            service_principal: "HTTP/server.example.com".into(),
1648            ..Default::default()
1649        };
1650        let err = KerberosManager::new(config).unwrap_err();
1651        assert!(err.to_string().contains("realm"));
1652    }
1653
1654    #[test]
1655    fn test_manager_creation() {
1656        let config = KerberosConfig {
1657            service_principal: "HTTP/server.example.com@EXAMPLE.COM".into(),
1658            realm: "EXAMPLE.COM".into(),
1659            ..Default::default()
1660        };
1661        let mgr = KerberosManager::new(config);
1662        assert!(mgr.is_ok());
1663    }
1664
1665    #[test]
1666    fn test_generate_challenge() {
1667        let config = KerberosConfig {
1668            service_principal: "HTTP/server.example.com@EXAMPLE.COM".into(),
1669            realm: "EXAMPLE.COM".into(),
1670            ..Default::default()
1671        };
1672        let mgr = KerberosManager::new(config).unwrap();
1673        assert_eq!(mgr.generate_challenge(), "Negotiate");
1674    }
1675
1676    #[tokio::test]
1677    async fn test_replay_detection() {
1678        let config = KerberosConfig {
1679            service_principal: "HTTP/server.example.com@EXAMPLE.COM".into(),
1680            realm: "EXAMPLE.COM".into(),
1681            ..Default::default()
1682        };
1683        let mgr = KerberosManager::new(config).unwrap();
1684
1685        let token_data = b"test_token_data";
1686
1687        // First check should succeed
1688        mgr.check_replay(token_data).await.unwrap();
1689
1690        // Second check with same data should fail (replay)
1691        let err = mgr.check_replay(token_data).await.unwrap_err();
1692        assert!(err.to_string().contains("replay"));
1693    }
1694
1695    #[test]
1696    fn test_invalid_keytab() {
1697        let result = parse_keytab(&[0x00, 0x01]);
1698        assert!(result.is_err());
1699    }
1700
1701    #[test]
1702    fn test_spnego_state_variants() {
1703        assert_eq!(SpnegoState::Initial, SpnegoState::Initial);
1704        assert_ne!(SpnegoState::Initial, SpnegoState::Completed);
1705    }
1706
1707    // ── DER parser tests ─────────────────────────────────────────────────
1708
1709    #[test]
1710    fn test_parse_der_integer() {
1711        // DER INTEGER: 02 01 05 → value = 5
1712        let data = [0x02, 0x01, 0x05];
1713        let (tlv, rest) = parse_der(&data).unwrap();
1714        assert!(rest.is_empty());
1715        assert_eq!(tlv.class, 0); // Universal
1716        assert_eq!(tlv.tag_num, 2); // INTEGER
1717        assert_eq!(parse_der_integer(tlv.value).unwrap(), 5);
1718    }
1719
1720    #[test]
1721    fn test_parse_der_sequence() {
1722        // SEQUENCE { INTEGER 5, INTEGER 14 }
1723        // 30 06 02 01 05 02 01 0e
1724        let data = [0x30, 0x06, 0x02, 0x01, 0x05, 0x02, 0x01, 0x0e];
1725        let (tlv, _) = parse_der(&data).unwrap();
1726        assert_eq!(tlv.tag_num, 16); // SEQUENCE
1727        assert!(tlv.constructed);
1728        let contents = parse_der_contents(tlv.value).unwrap();
1729        assert_eq!(contents.len(), 2);
1730        assert_eq!(parse_der_integer(contents[0].value).unwrap(), 5);
1731        assert_eq!(parse_der_integer(contents[1].value).unwrap(), 14);
1732    }
1733
1734    #[test]
1735    fn test_parse_der_context_tag() {
1736        // [0] EXPLICIT INTEGER 5 → a0 03 02 01 05
1737        let data = [0xa0, 0x03, 0x02, 0x01, 0x05];
1738        let (tlv, _) = parse_der(&data).unwrap();
1739        assert_eq!(tlv.class, 2); // Context-specific
1740        assert_eq!(tlv.tag_num, 0);
1741        let inner = unwrap_explicit(&tlv).unwrap();
1742        assert_eq!(parse_der_integer(inner.value).unwrap(), 5);
1743    }
1744
1745    #[test]
1746    fn test_parse_der_oid() {
1747        // OID 1.3.6.1.5.5.2 (SPNEGO)
1748        let data = [0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
1749        let (tlv, _) = parse_der(&data).unwrap();
1750        assert!(oid_matches(&tlv, SPNEGO_OID_BYTES));
1751        assert!(!oid_matches(&tlv, KRB5_OID_BYTES));
1752    }
1753
1754    #[test]
1755    fn test_parse_der_truncated_fails() {
1756        assert!(parse_der(&[]).is_err());
1757        assert!(parse_der(&[0x02]).is_err()); // missing length
1758        assert!(parse_der(&[0x02, 0x05, 0x01]).is_err()); // length says 5, only 1 byte
1759    }
1760
1761    // ── n-fold tests (RFC 3961 test vectors) ─────────────────────────────
1762
1763    #[test]
1764    fn test_nfold_64bit() {
1765        // n-fold("012345", 64) = be072631276b1955 (RFC 3961 §A.1)
1766        let result = nfold(b"012345", 8);
1767        assert_eq!(result, vec![0xBE, 0x07, 0x26, 0x31, 0x27, 0x6B, 0x19, 0x55]);
1768    }
1769
1770    #[test]
1771    fn test_nfold_56bit() {
1772        // n-fold("password", 56) = 78 A0 7B 6C AF 85 FA
1773        let result = nfold(b"password", 7);
1774        assert_eq!(result, vec![0x78, 0xA0, 0x7B, 0x6C, 0xAF, 0x85, 0xFA]);
1775    }
1776
1777    #[test]
1778    fn test_nfold_64bit_long_input() {
1779        // n-fold("Rough Consensus, and Running Code", 64) = bb6ed30870b7f0e0 (RFC 3961 §A.1)
1780        let result = nfold(b"Rough Consensus, and Running Code", 8);
1781        assert_eq!(result, vec![0xBB, 0x6E, 0xD3, 0x08, 0x70, 0xB7, 0xF0, 0xE0]);
1782    }
1783
1784    #[test]
1785    fn test_nfold_168bit() {
1786        // n-fold("password", 168) = 59e4a8ca7c0385c3c37b3f6d2000247cb6e6bd5b3e (RFC 3961 §A.1)
1787        let result = nfold(b"password", 21);
1788        assert_eq!(
1789            result,
1790            vec![
1791                0x59, 0xE4, 0xA8, 0xCA, 0x7C, 0x03, 0x85, 0xC3, 0xC3, 0x7B, 0x3F, 0x6D, 0x20, 0x00,
1792                0x24, 0x7C, 0xB6, 0xE6, 0xBD, 0x5B, 0x3E,
1793            ]
1794        );
1795    }
1796
1797    // ── AES crypto tests ─────────────────────────────────────────────────
1798
1799    #[test]
1800    fn test_aes_ecb_roundtrip() {
1801        let key = [0x42u8; 16];
1802        let block = [0x01u8; 16];
1803        let encrypted = aes_ecb_encrypt(&key, &block);
1804        let decrypted = aes_ecb_decrypt(&key, &encrypted);
1805        assert_eq!(decrypted, block);
1806    }
1807
1808    #[test]
1809    fn test_aes_ecb_256_roundtrip() {
1810        let key = [0x42u8; 32];
1811        let block = [0xABu8; 16];
1812        let encrypted = aes_ecb_encrypt(&key, &block);
1813        let decrypted = aes_ecb_decrypt(&key, &encrypted);
1814        assert_eq!(decrypted, block);
1815    }
1816
1817    #[test]
1818    fn test_aes_cbc_decrypt_two_blocks() {
1819        let key = [0x00u8; 16];
1820        // Encrypt two zero blocks with zero key and zero IV using CBC
1821        let p0 = [0u8; 16];
1822        let p1 = [0u8; 16];
1823        // CBC encrypt: C0 = E(P0 XOR 0) = E(0), C1 = E(P1 XOR C0)
1824        let c0 = aes_ecb_encrypt(&key, &p0);
1825        let p1_xor_c0: [u8; 16] = xor_bytes(&p1, &c0).try_into().unwrap();
1826        let c1 = aes_ecb_encrypt(&key, &p1_xor_c0);
1827        let mut ciphertext = Vec::new();
1828        ciphertext.extend_from_slice(&c0);
1829        ciphertext.extend_from_slice(&c1);
1830
1831        let plaintext = aes_cbc_decrypt(&key, &ciphertext).unwrap();
1832        let mut expected = Vec::new();
1833        expected.extend_from_slice(&p0);
1834        expected.extend_from_slice(&p1);
1835        assert_eq!(plaintext, expected);
1836    }
1837
1838    #[test]
1839    fn test_aes_cts_decrypt_single_block() {
1840        let key = [0x00u8; 16];
1841        let plaintext = [0x42u8; 16];
1842        // Single block: CTS = just ECB
1843        let ciphertext = aes_ecb_encrypt(&key, &plaintext);
1844        let decrypted = aes_cts_decrypt(&key, &ciphertext).unwrap();
1845        assert_eq!(&decrypted[..], &plaintext[..]);
1846    }
1847
1848    #[test]
1849    fn test_derive_key_produces_correct_length() {
1850        let key_128 = [0x42u8; 16];
1851        let derived = derive_key_aes(&key_128, 2, 0xAA);
1852        assert_eq!(derived.len(), 16);
1853
1854        let key_256 = [0x42u8; 32];
1855        let derived = derive_key_aes(&key_256, 2, 0xAA);
1856        assert_eq!(derived.len(), 32);
1857    }
1858
1859    #[test]
1860    fn test_hmac_sha1_produces_20_bytes() {
1861        let result = hmac_sha1(b"key", b"data");
1862        assert_eq!(result.len(), 20);
1863    }
1864
1865    // ── Keytab tests ─────────────────────────────────────────────────────
1866
1867    #[test]
1868    fn test_valid_keytab_v2_header() {
1869        // Minimal valid keytab: version 0x0502, followed by zero-length remaining data
1870        let data = [0x05, 0x02, 0x00, 0x00];
1871        let entries = parse_keytab(&data).unwrap();
1872        assert!(entries.is_empty());
1873    }
1874
1875    // ── Ticket flag tests ────────────────────────────────────────────────
1876
1877    #[test]
1878    fn test_ticket_flags_parsing() {
1879        // Bit 1 = forwardable, bit 8 = renewable
1880        // Bit 1 in byte 0 is position 6 (0x40), bit 8 in byte 1 is position 7 (0x80)
1881        let flags = [0x00, 0x40, 0x80, 0x00, 0x00]; // [unused_bits=0] [byte0] [byte1] ...
1882        assert!(test_bit_flag(&flags, 1)); // forwardable
1883        assert!(!test_bit_flag(&flags, 2)); // forwarded
1884        assert!(test_bit_flag(&flags, 8)); // renewable
1885        assert!(!test_bit_flag(&flags, 10)); // pre_authent
1886    }
1887
1888    // ── Builder & preset tests ───────────────────────────────────────────
1889
1890    #[test]
1891    fn test_kerberos_config_builder() {
1892        let config = KerberosConfig::builder("HTTP/srv@REALM", "REALM")
1893            .keytab_path("/etc/krb5.keytab")
1894            .add_kdc("kdc1:88")
1895            .add_kdc("kdc2:88")
1896            .max_clock_skew_secs(600)
1897            .build();
1898
1899        assert_eq!(config.service_principal, "HTTP/srv@REALM");
1900        assert_eq!(config.realm, "REALM");
1901        assert_eq!(config.keytab_path.as_deref(), Some("/etc/krb5.keytab"));
1902        assert_eq!(config.kdc_addresses, vec!["kdc1:88", "kdc2:88"]);
1903        assert_eq!(config.max_clock_skew_secs, 600);
1904        assert!(!config.allow_delegation);
1905    }
1906
1907    #[test]
1908    fn test_kerberos_config_active_directory() {
1909        let config = KerberosConfig::active_directory("HTTP/srv@AD.COM", "AD.COM");
1910        assert_eq!(config.service_principal, "HTTP/srv@AD.COM");
1911        assert_eq!(config.realm, "AD.COM");
1912        assert!(config.allow_delegation);
1913        // Should produce a valid manager
1914        let mgr = KerberosManager::new(config);
1915        assert!(mgr.is_ok());
1916    }
1917
1918    #[test]
1919    fn test_kerberos_builder_override() {
1920        let config = KerberosConfig::builder("HTTP/srv@REALM", "REALM")
1921            .allow_delegation(true)
1922            .replay_cache_max_entries(500)
1923            .build();
1924
1925        assert!(config.allow_delegation);
1926        assert_eq!(config.replay_cache_max_entries, 500);
1927    }
1928}