⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/net/kerberos/
ccache.rs

1//! Kerberos Ticket Cache (ccache)
2//!
3//! Implements an in-memory credential cache for Kerberos tickets, compatible
4//! with the MIT krb5 ccache binary format for serialization/deserialization.
5//!
6//! # Features
7//!
8//! - Store/lookup/remove tickets by server principal
9//! - TTL-based expiry using kernel tick counts
10//! - MIT krb5 ccache v4 binary format serialization
11//! - Shell command helpers: kinit, klist, kdestroy
12//! - Auth backend integration for ticket-based verification
13
14#![allow(dead_code)]
15
16#[cfg(feature = "alloc")]
17extern crate alloc;
18
19#[cfg(feature = "alloc")]
20use alloc::{string::String, vec::Vec};
21
22use super::protocol::{EncryptionType, KerberosClient, KerberosTime, PrincipalName};
23use crate::error::KernelError;
24
25// ---------------------------------------------------------------------------
26// Ccache Constants
27// ---------------------------------------------------------------------------
28
29/// MIT krb5 ccache file format version (0x0504)
30const CCACHE_VERSION: u16 = 0x0504;
31
32/// Maximum number of cached tickets
33const MAX_CACHE_ENTRIES: usize = 64;
34
35// ---------------------------------------------------------------------------
36// Ccache Entry
37// ---------------------------------------------------------------------------
38
39/// A single cached Kerberos ticket.
40#[cfg(feature = "alloc")]
41#[derive(Debug, Clone)]
42pub struct CcacheEntry {
43    /// Client principal
44    pub client_principal: PrincipalName,
45    /// Server principal (service/host)
46    pub server_principal: PrincipalName,
47    /// Session key (decrypted)
48    pub session_key: Vec<u8>,
49    /// Session key encryption type
50    pub session_key_etype: EncryptionType,
51    /// Authentication time
52    pub auth_time: KerberosTime,
53    /// Start time (when ticket becomes valid)
54    pub start_time: KerberosTime,
55    /// End time (when ticket expires)
56    pub end_time: KerberosTime,
57    /// Renewal end time
58    pub renew_till: Option<KerberosTime>,
59    /// Raw ticket data (BER-encoded Ticket)
60    pub ticket_data: Vec<u8>,
61    /// Ticket flags
62    pub flags: u32,
63}
64
65#[cfg(feature = "alloc")]
66impl CcacheEntry {
67    /// Check if this ticket has expired.
68    pub fn is_expired(&self) -> bool {
69        self.end_time.has_expired()
70    }
71
72    /// Check if this ticket is renewable and the renewal window is still open.
73    pub fn is_renewable(&self) -> bool {
74        if let Some(ref renew_till) = self.renew_till {
75            !renew_till.has_expired()
76        } else {
77            false
78        }
79    }
80
81    /// Check if this entry matches a server principal.
82    pub fn matches_server(&self, server: &PrincipalName) -> bool {
83        self.server_principal == *server
84    }
85
86    /// Remaining lifetime in seconds (0 if expired).
87    pub fn remaining_secs(&self) -> u64 {
88        let now = crate::arch::timer::get_timestamp_secs();
89        self.end_time.timestamp.saturating_sub(now)
90    }
91}
92
93// ---------------------------------------------------------------------------
94// Ccache File Format
95// ---------------------------------------------------------------------------
96
97/// MIT krb5 ccache file structure.
98///
99/// Format (v4):
100/// ```text
101/// [2 bytes] version (0x0504)
102/// [2 bytes] header length
103/// [N bytes] header tags
104/// [principal] default principal
105/// [credentials...] repeated credential entries
106/// ```
107#[cfg(feature = "alloc")]
108#[derive(Debug, Clone)]
109pub struct CcacheFile {
110    /// File format version
111    pub version: u16,
112    /// Default principal
113    pub default_principal: PrincipalName,
114    /// Cached credentials
115    pub entries: Vec<CcacheEntry>,
116}
117
118#[cfg(feature = "alloc")]
119impl CcacheFile {
120    /// Create a new empty ccache file.
121    pub fn new(default_principal: PrincipalName) -> Self {
122        Self {
123            version: CCACHE_VERSION,
124            default_principal,
125            entries: Vec::new(),
126        }
127    }
128
129    /// Serialize to MIT krb5 ccache binary format.
130    pub fn serialize(&self) -> Vec<u8> {
131        let mut out = Vec::new();
132
133        // Version (big-endian u16)
134        out.extend_from_slice(&self.version.to_be_bytes());
135
136        // Header length (v4 has a header section)
137        let header_len: u16 = 0; // no extra header tags
138        out.extend_from_slice(&header_len.to_be_bytes());
139
140        // Default principal
141        Self::serialize_principal(&self.default_principal, &mut out);
142
143        // Credentials
144        for entry in &self.entries {
145            self.serialize_credential(entry, &mut out);
146        }
147
148        out
149    }
150
151    /// Deserialize from MIT krb5 ccache binary format.
152    pub fn deserialize(data: &[u8]) -> Result<Self, KernelError> {
153        if data.len() < 4 {
154            return Err(KernelError::InvalidArgument {
155                name: "ccache",
156                value: "too short",
157            });
158        }
159
160        let mut pos = 0;
161
162        // Version
163        let version = u16::from_be_bytes([data[pos], data[pos + 1]]);
164        pos += 2;
165
166        if version != CCACHE_VERSION {
167            return Err(KernelError::InvalidArgument {
168                name: "ccache_version",
169                value: "unsupported version",
170            });
171        }
172
173        // Header length
174        let header_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
175        pos += 2;
176
177        // Skip header tags
178        if pos + header_len > data.len() {
179            return Err(KernelError::InvalidArgument {
180                name: "ccache_header",
181                value: "truncated",
182            });
183        }
184        pos += header_len;
185
186        // Default principal
187        let (default_principal, consumed) = Self::deserialize_principal(data, pos)?;
188        pos += consumed;
189
190        // Credentials
191        let mut entries = Vec::new();
192        while pos < data.len() {
193            match Self::deserialize_credential(data, pos) {
194                Ok((entry, consumed)) => {
195                    entries.push(entry);
196                    pos += consumed;
197                }
198                Err(_) => break,
199            }
200        }
201
202        Ok(Self {
203            version,
204            default_principal,
205            entries,
206        })
207    }
208
209    /// Serialize a principal name.
210    fn serialize_principal(principal: &PrincipalName, out: &mut Vec<u8>) {
211        // name_type (u32 big-endian)
212        out.extend_from_slice(&(principal.name_type as u32).to_be_bytes());
213        // num_components (u32 big-endian)
214        out.extend_from_slice(&(principal.name_string.len() as u32).to_be_bytes());
215        // Each component: [u32 length] [bytes]
216        for component in &principal.name_string {
217            out.extend_from_slice(&(component.len() as u32).to_be_bytes());
218            out.extend_from_slice(component.as_bytes());
219        }
220    }
221
222    /// Deserialize a principal name.
223    fn deserialize_principal(
224        data: &[u8],
225        start: usize,
226    ) -> Result<(PrincipalName, usize), KernelError> {
227        let mut pos = start;
228
229        if pos + 8 > data.len() {
230            return Err(KernelError::InvalidArgument {
231                name: "ccache_principal",
232                value: "truncated",
233            });
234        }
235
236        let name_type_val =
237            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
238        pos += 4;
239
240        let num_components =
241            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
242        pos += 4;
243
244        let mut name_string = Vec::new();
245        for _ in 0..num_components {
246            if pos + 4 > data.len() {
247                return Err(KernelError::InvalidArgument {
248                    name: "ccache_component",
249                    value: "truncated",
250                });
251            }
252            let comp_len =
253                u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
254                    as usize;
255            pos += 4;
256
257            if pos + comp_len > data.len() {
258                return Err(KernelError::InvalidArgument {
259                    name: "ccache_component_data",
260                    value: "truncated",
261                });
262            }
263            let s = core::str::from_utf8(&data[pos..pos + comp_len]).map_err(|_| {
264                KernelError::InvalidArgument {
265                    name: "ccache_component",
266                    value: "invalid utf8",
267                }
268            })?;
269            name_string.push(String::from(s));
270            pos += comp_len;
271        }
272
273        let name_type = match name_type_val {
274            1 => super::protocol::NameType::Principal,
275            2 => super::protocol::NameType::SrvInst,
276            3 => super::protocol::NameType::SrvHst,
277            _ => super::protocol::NameType::Principal,
278        };
279
280        Ok((
281            PrincipalName {
282                name_type,
283                name_string,
284            },
285            pos - start,
286        ))
287    }
288
289    /// Serialize a credential entry.
290    fn serialize_credential(&self, entry: &CcacheEntry, out: &mut Vec<u8>) {
291        // Client principal
292        Self::serialize_principal(&entry.client_principal, out);
293        // Server principal
294        Self::serialize_principal(&entry.server_principal, out);
295        // Session key: [u16 etype] [u32 len] [bytes]
296        out.extend_from_slice(&(entry.session_key_etype as u16).to_be_bytes());
297        out.extend_from_slice(&(entry.session_key.len() as u32).to_be_bytes());
298        out.extend_from_slice(&entry.session_key);
299        // Times: auth_time, start_time, end_time, renew_till (each u32)
300        out.extend_from_slice(&(entry.auth_time.timestamp as u32).to_be_bytes());
301        out.extend_from_slice(&(entry.start_time.timestamp as u32).to_be_bytes());
302        out.extend_from_slice(&(entry.end_time.timestamp as u32).to_be_bytes());
303        let renew = entry.renew_till.map_or(0u32, |t| t.timestamp as u32);
304        out.extend_from_slice(&renew.to_be_bytes());
305        // Flags (u32)
306        out.extend_from_slice(&entry.flags.to_be_bytes());
307        // Ticket data: [u32 len] [bytes]
308        out.extend_from_slice(&(entry.ticket_data.len() as u32).to_be_bytes());
309        out.extend_from_slice(&entry.ticket_data);
310    }
311
312    /// Deserialize a credential entry.
313    fn deserialize_credential(
314        data: &[u8],
315        start: usize,
316    ) -> Result<(CcacheEntry, usize), KernelError> {
317        let mut pos = start;
318
319        // Client principal
320        let (client_principal, consumed) = Self::deserialize_principal(data, pos)?;
321        pos += consumed;
322
323        // Server principal
324        let (server_principal, consumed) = Self::deserialize_principal(data, pos)?;
325        pos += consumed;
326
327        // Session key
328        if pos + 6 > data.len() {
329            return Err(KernelError::InvalidArgument {
330                name: "ccache_cred",
331                value: "truncated key",
332            });
333        }
334        let etype_val = u16::from_be_bytes([data[pos], data[pos + 1]]);
335        pos += 2;
336        let key_len =
337            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
338        pos += 4;
339
340        if pos + key_len > data.len() {
341            return Err(KernelError::InvalidArgument {
342                name: "ccache_cred",
343                value: "truncated key data",
344            });
345        }
346        let session_key = data[pos..pos + key_len].to_vec();
347        pos += key_len;
348
349        // Times (4 x u32)
350        if pos + 16 > data.len() {
351            return Err(KernelError::InvalidArgument {
352                name: "ccache_cred",
353                value: "truncated times",
354            });
355        }
356        let auth_time =
357            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
358        pos += 4;
359        let start_time =
360            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
361        pos += 4;
362        let end_time = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
363        pos += 4;
364        let renew_till_val =
365            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
366        pos += 4;
367
368        // Flags
369        if pos + 4 > data.len() {
370            return Err(KernelError::InvalidArgument {
371                name: "ccache_cred",
372                value: "truncated flags",
373            });
374        }
375        let flags = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
376        pos += 4;
377
378        // Ticket data
379        if pos + 4 > data.len() {
380            return Err(KernelError::InvalidArgument {
381                name: "ccache_cred",
382                value: "truncated ticket len",
383            });
384        }
385        let ticket_len =
386            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
387        pos += 4;
388
389        if pos + ticket_len > data.len() {
390            return Err(KernelError::InvalidArgument {
391                name: "ccache_cred",
392                value: "truncated ticket data",
393            });
394        }
395        let ticket_data = data[pos..pos + ticket_len].to_vec();
396        pos += ticket_len;
397
398        let session_key_etype =
399            EncryptionType::from_i64(etype_val as i64).unwrap_or(EncryptionType::Aes256CtsHmacSha1);
400
401        let renew_till = if renew_till_val > 0 {
402            Some(KerberosTime::from_timestamp(renew_till_val as u64))
403        } else {
404            None
405        };
406
407        Ok((
408            CcacheEntry {
409                client_principal,
410                server_principal,
411                session_key,
412                session_key_etype,
413                auth_time: KerberosTime::from_timestamp(auth_time as u64),
414                start_time: KerberosTime::from_timestamp(start_time as u64),
415                end_time: KerberosTime::from_timestamp(end_time as u64),
416                renew_till,
417                ticket_data,
418                flags,
419            },
420            pos - start,
421        ))
422    }
423}
424
425// ---------------------------------------------------------------------------
426// Ticket Cache (in-memory)
427// ---------------------------------------------------------------------------
428
429/// In-memory Kerberos ticket cache.
430///
431/// Provides store/lookup/remove operations with automatic expiry purging.
432#[cfg(feature = "alloc")]
433pub struct TicketCache {
434    /// Default principal (the authenticated user)
435    default_principal: Option<PrincipalName>,
436    /// Cached ticket entries
437    entries: Vec<CcacheEntry>,
438}
439
440#[cfg(feature = "alloc")]
441impl Default for TicketCache {
442    fn default() -> Self {
443        Self::new()
444    }
445}
446
447#[cfg(feature = "alloc")]
448impl TicketCache {
449    /// Create a new empty ticket cache.
450    pub fn new() -> Self {
451        Self {
452            default_principal: None,
453            entries: Vec::new(),
454        }
455    }
456
457    /// Set the default principal.
458    pub fn set_default_principal(&mut self, principal: PrincipalName) {
459        self.default_principal = Some(principal);
460    }
461
462    /// Get the default principal.
463    pub fn default_principal(&self) -> Option<&PrincipalName> {
464        self.default_principal.as_ref()
465    }
466
467    /// Store a ticket in the cache.
468    ///
469    /// If a ticket for the same server principal already exists, it is
470    /// replaced.
471    pub fn store_ticket(&mut self, entry: CcacheEntry) {
472        // Replace existing entry for the same server principal
473        if let Some(existing) = self
474            .entries
475            .iter_mut()
476            .find(|e| e.server_principal == entry.server_principal)
477        {
478            *existing = entry;
479            return;
480        }
481
482        // Evict oldest if at capacity
483        if self.entries.len() >= MAX_CACHE_ENTRIES {
484            self.entries.remove(0);
485        }
486
487        self.entries.push(entry);
488    }
489
490    /// Look up a ticket by server principal.
491    pub fn lookup_ticket(&self, server: &PrincipalName) -> Option<&CcacheEntry> {
492        self.entries
493            .iter()
494            .find(|e| e.matches_server(server) && !e.is_expired())
495    }
496
497    /// Remove a ticket by server principal.
498    pub fn remove_ticket(&mut self, server: &PrincipalName) -> bool {
499        let initial_len = self.entries.len();
500        self.entries.retain(|e| !e.matches_server(server));
501        self.entries.len() < initial_len
502    }
503
504    /// Remove all expired tickets.
505    pub fn purge_expired(&mut self) -> usize {
506        let initial_len = self.entries.len();
507        self.entries.retain(|e| !e.is_expired());
508        initial_len - self.entries.len()
509    }
510
511    /// List all cached tickets with metadata.
512    pub fn list_tickets(&self) -> &[CcacheEntry] {
513        &self.entries
514    }
515
516    /// Get the number of cached tickets.
517    pub fn len(&self) -> usize {
518        self.entries.len()
519    }
520
521    /// Check if the cache is empty.
522    pub fn is_empty(&self) -> bool {
523        self.entries.is_empty()
524    }
525
526    /// Clear all entries (kdestroy).
527    pub fn clear(&mut self) {
528        self.entries.clear();
529        self.default_principal = None;
530    }
531
532    /// Export to ccache binary format.
533    pub fn serialize(&self) -> Option<Vec<u8>> {
534        let principal = self.default_principal.as_ref()?;
535        let file = CcacheFile {
536            version: CCACHE_VERSION,
537            default_principal: principal.clone(),
538            entries: self.entries.clone(),
539        };
540        Some(file.serialize())
541    }
542
543    /// Import from ccache binary format.
544    pub fn deserialize(data: &[u8]) -> Result<Self, KernelError> {
545        let file = CcacheFile::deserialize(data)?;
546        Ok(Self {
547            default_principal: Some(file.default_principal),
548            entries: file.entries,
549        })
550    }
551}
552
553// ---------------------------------------------------------------------------
554// Shell Command Helpers
555// ---------------------------------------------------------------------------
556
557/// Perform a kinit-like operation: derive key, request TGT, store in cache.
558///
559/// Returns the encoded AS-REQ bytes (caller must send to KDC and feed
560/// the response back via `process_kinit_response`).
561#[cfg(feature = "alloc")]
562pub fn kinit_command(
563    username: &str,
564    realm: &str,
565    password: &str,
566    cache: &mut TicketCache,
567) -> Vec<u8> {
568    let mut client = KerberosClient::new(username, realm, password);
569    let principal = PrincipalName::new_principal(username);
570    cache.set_default_principal(principal);
571    client.request_tgt()
572}
573
574/// Format cached tickets for display (klist).
575#[cfg(feature = "alloc")]
576pub fn klist_command(cache: &TicketCache) -> Vec<String> {
577    let mut lines = Vec::new();
578
579    if let Some(principal) = cache.default_principal() {
580        let mut header = String::from("Default principal: ");
581        header.push_str(&principal.to_text());
582        lines.push(header);
583    } else {
584        lines.push(String::from("No default principal"));
585    }
586
587    lines.push(String::new());
588
589    if cache.is_empty() {
590        lines.push(String::from("No cached tickets"));
591        return lines;
592    }
593
594    lines.push(String::from(
595        "  Server                          Expires         Flags",
596    ));
597    lines.push(String::from(
598        "  ------                          -------         -----",
599    ));
600
601    for entry in cache.list_tickets() {
602        let server = entry.server_principal.to_text();
603        let remaining = entry.remaining_secs();
604        let expired_marker = if entry.is_expired() { " [EXPIRED]" } else { "" };
605
606        let mut line = String::from("  ");
607        line.push_str(&server);
608
609        // Pad to alignment
610        let pad = if server.len() < 32 {
611            32 - server.len()
612        } else {
613            2
614        };
615        for _ in 0..pad {
616            line.push(' ');
617        }
618
619        // Remaining time
620        let hours = remaining / 3600;
621        let minutes = (remaining % 3600) / 60;
622        let mut time_str = String::new();
623        // Manual integer-to-string formatting
624        push_u64(&mut time_str, hours);
625        time_str.push('h');
626        push_u64(&mut time_str, minutes);
627        time_str.push('m');
628        line.push_str(&time_str);
629        line.push_str(expired_marker);
630
631        lines.push(line);
632    }
633
634    lines
635}
636
637/// Helper: push a u64 value as decimal digits to a String.
638#[cfg(feature = "alloc")]
639fn push_u64(s: &mut String, mut val: u64) {
640    if val == 0 {
641        s.push('0');
642        return;
643    }
644    let mut digits = [0u8; 20];
645    let mut count = 0;
646    while val > 0 {
647        digits[count] = (val % 10) as u8;
648        val /= 10;
649        count += 1;
650    }
651    for i in (0..count).rev() {
652        s.push((b'0' + digits[i]) as char);
653    }
654}
655
656/// Destroy all cached tickets (kdestroy).
657#[cfg(feature = "alloc")]
658pub fn kdestroy_command(cache: &mut TicketCache) {
659    cache.clear();
660}
661
662// ---------------------------------------------------------------------------
663// Kerberos Auth Backend
664// ---------------------------------------------------------------------------
665
666/// Authentication backend that verifies credentials via Kerberos ticket
667/// presence in the cache.
668#[cfg(feature = "alloc")]
669pub struct KerberosAuthBackend {
670    /// Realm for this backend
671    realm: String,
672}
673
674#[cfg(feature = "alloc")]
675impl KerberosAuthBackend {
676    /// Create a new Kerberos auth backend.
677    pub fn new(realm: &str) -> Self {
678        Self {
679            realm: String::from(realm),
680        }
681    }
682
683    /// Check if a user has a valid TGT in the cache.
684    pub fn has_valid_ticket(&self, cache: &TicketCache, username: &str) -> bool {
685        let krbtgt = PrincipalName::krbtgt(&self.realm);
686        if let Some(entry) = cache.lookup_ticket(&krbtgt) {
687            // Verify the client principal matches
688            if entry
689                .client_principal
690                .name_string
691                .first()
692                .map(|s| s.as_str())
693                == Some(username)
694            {
695                return !entry.is_expired();
696            }
697        }
698        false
699    }
700
701    /// Get the realm.
702    pub fn realm(&self) -> &str {
703        &self.realm
704    }
705}
706
707// ---------------------------------------------------------------------------
708// Tests
709// ---------------------------------------------------------------------------
710
711#[cfg(test)]
712mod tests {
713    #[allow(unused_imports)]
714    use alloc::vec;
715
716    use super::*;
717
718    fn make_test_entry(server: &str, end_secs: u64) -> CcacheEntry {
719        CcacheEntry {
720            client_principal: PrincipalName::new_principal("testuser"),
721            server_principal: PrincipalName::new_service("krbtgt", server),
722            session_key: vec![0x42; 32],
723            session_key_etype: EncryptionType::Aes256CtsHmacSha1,
724            auth_time: KerberosTime::from_timestamp(1000),
725            start_time: KerberosTime::from_timestamp(1000),
726            end_time: KerberosTime::from_timestamp(end_secs),
727            renew_till: None,
728            ticket_data: vec![0xDE, 0xAD],
729            flags: 0x4000_0000,
730        }
731    }
732
733    #[test]
734    fn test_ticket_cache_store_lookup() {
735        let mut cache = TicketCache::new();
736        let entry = make_test_entry("EXAMPLE.COM", u64::MAX);
737        cache.store_ticket(entry);
738
739        let server = PrincipalName::new_service("krbtgt", "EXAMPLE.COM");
740        let found = cache.lookup_ticket(&server);
741        assert!(found.is_some());
742        assert_eq!(found.unwrap().session_key.len(), 32);
743    }
744
745    #[test]
746    fn test_ticket_cache_remove() {
747        let mut cache = TicketCache::new();
748        cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX));
749
750        let server = PrincipalName::new_service("krbtgt", "EXAMPLE.COM");
751        assert!(cache.remove_ticket(&server));
752        assert!(cache.is_empty());
753    }
754
755    #[test]
756    fn test_ticket_cache_replace_existing() {
757        let mut cache = TicketCache::new();
758        // Use far-future timestamps to avoid expiration in test
759        cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX / 2));
760        cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX / 2 + 1));
761
762        assert_eq!(cache.len(), 1);
763        let server = PrincipalName::new_service("krbtgt", "EXAMPLE.COM");
764        let found = cache.lookup_ticket(&server).unwrap();
765        assert_eq!(found.end_time.timestamp, u64::MAX / 2 + 1);
766    }
767
768    #[test]
769    fn test_ticket_cache_clear() {
770        let mut cache = TicketCache::new();
771        cache.set_default_principal(PrincipalName::new_principal("alice"));
772        cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX));
773        cache.store_ticket(make_test_entry("OTHER.COM", u64::MAX));
774
775        assert_eq!(cache.len(), 2);
776        cache.clear();
777        assert!(cache.is_empty());
778        assert!(cache.default_principal().is_none());
779    }
780
781    #[test]
782    fn test_ccache_serialize_deserialize() {
783        let principal = PrincipalName::new_principal("alice");
784        let mut file = CcacheFile::new(principal.clone());
785        file.entries.push(make_test_entry("EXAMPLE.COM", 5000));
786
787        let serialized = file.serialize();
788        assert!(!serialized.is_empty());
789
790        let deserialized = CcacheFile::deserialize(&serialized).unwrap();
791        assert_eq!(deserialized.version, CCACHE_VERSION);
792        assert_eq!(deserialized.default_principal.name_string[0], "alice");
793        assert_eq!(deserialized.entries.len(), 1);
794    }
795
796    #[test]
797    fn test_ccache_roundtrip_multiple_entries() {
798        let principal = PrincipalName::new_principal("bob");
799        let mut file = CcacheFile::new(principal);
800        file.entries.push(make_test_entry("REALM1.COM", 5000));
801        file.entries.push(make_test_entry("REALM2.COM", 6000));
802
803        let serialized = file.serialize();
804        let deserialized = CcacheFile::deserialize(&serialized).unwrap();
805        assert_eq!(deserialized.entries.len(), 2);
806        assert_eq!(
807            deserialized.entries[0].server_principal.name_string[1],
808            "REALM1.COM"
809        );
810        assert_eq!(
811            deserialized.entries[1].server_principal.name_string[1],
812            "REALM2.COM"
813        );
814    }
815
816    #[test]
817    fn test_kinit_produces_bytes() {
818        let mut cache = TicketCache::new();
819        let req = kinit_command("alice", "EXAMPLE.COM", "password", &mut cache);
820        assert!(!req.is_empty());
821        assert!(cache.default_principal().is_some());
822    }
823
824    #[test]
825    fn test_klist_empty_cache() {
826        let cache = TicketCache::new();
827        let lines = klist_command(&cache);
828        assert!(lines.iter().any(|l| l.contains("No cached tickets")));
829    }
830
831    #[test]
832    fn test_kdestroy() {
833        let mut cache = TicketCache::new();
834        cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX));
835        kdestroy_command(&mut cache);
836        assert!(cache.is_empty());
837    }
838
839    #[test]
840    fn test_auth_backend() {
841        let backend = KerberosAuthBackend::new("EXAMPLE.COM");
842        assert_eq!(backend.realm(), "EXAMPLE.COM");
843
844        let cache = TicketCache::new();
845        assert!(!backend.has_valid_ticket(&cache, "alice"));
846    }
847}