Skip to main content

netget/server/kerberos/
codec.rs

1//! Kerberos V5 (RFC 4120) wire helpers shared by the KDC and the client.
2//!
3//! The ASN.1 types are `picky-krb`'s; this module owns everything around them that has to be
4//! right for a peer we did not write:
5//!
6//! * **A bounded DER walk before every decode** ([`check_der`]). `picky-asn1-der` is a serde
7//!   deserializer and recurses with the nesting, so a peer that nests constructed elements
8//!   deeply would otherwise choose our stack depth. The walk is iterative, refuses indefinite
9//!   lengths, multi-byte tags, lengths past their parent, more than [`MAX_DER_ELEMENTS`]
10//!   elements and nesting past [`MAX_DER_DEPTH`], and requires exactly one element spanning the
11//!   whole buffer. Only then is the typed decoder run.
12//! * **Keys** ([`Key`]): AES-CTS-HMAC-SHA1-96 (RFC 3962), 128 and 256, derived with PBKDF2 from
13//!   a password and salt or drawn at random. Encryption is `picky-krb`'s; the integrity check on
14//!   decryption is computed here and compared in constant time. `Key` is deliberately not
15//!   `Debug`, and nothing in this module formats key bytes.
16//! * Times, flags, integers, principal names and the error-code vocabulary.
17
18use anyhow::{anyhow, Context, Result};
19use hmac::{Hmac, Mac};
20use picky_asn1::bit_string::BitString;
21use picky_asn1::date::Date;
22use picky_asn1::restricted_string::Ia5String;
23use picky_asn1::wrapper::{
24    Asn1SequenceOf, BitStringAsn1, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2,
25    GeneralStringAsn1, GeneralizedTimeAsn1, IntegerAsn1, OctetStringAsn1, Optional,
26};
27use picky_krb::crypto::{ChecksumSuite, CipherSuite};
28use picky_krb::data_types::{
29    EncryptedData, EtypeInfo2Entry, KerberosFlags, KerberosStringAsn1, KerberosTime, PaData,
30    PrincipalName,
31};
32use serde::de::DeserializeOwned;
33use serde::Serialize;
34
35/// The ASN.1 types this module builds on, re-exported so the client and the suites name them
36/// through NetGet's codec rather than reaching past it.
37pub mod types {
38    pub use picky_asn1::wrapper::*;
39    pub use picky_krb::data_types::*;
40    pub use picky_krb::messages::*;
41}
42
43/// The largest Kerberos message either role reads: a UDP datagram, or a TCP record's declared
44/// length. RFC 4120 sets no maximum; 64 KiB holds any ticket this KDC issues many times over and
45/// a PAC-bearing Active Directory TGT too.
46pub const MAX_MESSAGE_LEN: usize = 64 * 1024;
47
48/// Deepest constructed-element nesting accepted. The deepest legitimate path in a message this
49/// code decodes is a TGS-REQ's additional ticket, about 14 levels; 24 leaves room without
50/// letting a peer choose the decoder's stack depth.
51pub const MAX_DER_DEPTH: usize = 24;
52
53/// Most TLV elements accepted in one message, nested ones included.
54pub const MAX_DER_ELEMENTS: usize = 4096;
55
56/// Most components in a principal name (`host/fqdn` is two).
57pub const MAX_NAME_COMPONENTS: usize = 8;
58
59/// Longest single name component or realm, in bytes.
60pub const MAX_NAME_LEN: usize = 255;
61
62/// aes256-cts-hmac-sha1-96 (RFC 3962).
63pub const ETYPE_AES256: i32 = 18;
64/// aes128-cts-hmac-sha1-96 (RFC 3962).
65pub const ETYPE_AES128: i32 = 17;
66/// Every enctype NetGet speaks, strongest first. No DES, no RC4, no DES3.
67pub const SUPPORTED_ETYPES: [i32; 2] = [ETYPE_AES256, ETYPE_AES128];
68
69/// hmac-sha1-96-aes128 (RFC 3962 §7).
70pub const CKSUM_HMAC_SHA1_96_AES128: i32 = 15;
71/// hmac-sha1-96-aes256.
72pub const CKSUM_HMAC_SHA1_96_AES256: i32 = 16;
73
74/// PA-DATA types used here (RFC 4120 §7.5.2, RFC 6113).
75pub const PA_TGS_REQ: i32 = 1;
76pub const PA_ENC_TIMESTAMP: i32 = 2;
77pub const PA_ETYPE_INFO2: i32 = 19;
78
79/// Key usage numbers (RFC 4120 §7.5.1).
80pub const KU_PA_ENC_TIMESTAMP: i32 = 1;
81pub const KU_TICKET: i32 = 2;
82pub const KU_AS_REP_ENC_PART: i32 = 3;
83pub const KU_TGS_REQ_AUTH_CKSUM: i32 = 6;
84pub const KU_TGS_REQ_AUTHENTICATOR: i32 = 7;
85pub const KU_TGS_REP_ENC_SESSION_KEY: i32 = 8;
86pub const KU_TGS_REP_ENC_SUBKEY: i32 = 9;
87pub const KU_AP_REQ_AUTHENTICATOR: i32 = 11;
88
89/// Application tag bytes of the messages (constructed, application class).
90pub const TAG_AS_REQ: u8 = 0x6a;
91pub const TAG_AS_REP: u8 = 0x6b;
92pub const TAG_TGS_REQ: u8 = 0x6c;
93pub const TAG_TGS_REP: u8 = 0x6d;
94pub const TAG_AP_REQ: u8 = 0x6e;
95pub const TAG_KRB_ERROR: u8 = 0x7e;
96
97/// Name types (RFC 4120 §6.2).
98pub const NT_PRINCIPAL: i32 = 1;
99pub const NT_SRV_INST: i32 = 2;
100pub const NT_SRV_HST: i32 = 3;
101
102// ---------------------------------------------------------------------------------------------
103// Flag bits. RFC 4120 numbers bits from the most significant end; `bit(n)` is that bit in the
104// first 32 bits read as a big-endian integer, which is how both MIT and Heimdal store them.
105// ---------------------------------------------------------------------------------------------
106
107pub const fn bit(n: u32) -> u32 {
108    1 << (31 - n)
109}
110
111/// KDC options (RFC 4120 §5.4.1).
112pub mod kdc_options {
113    use super::bit;
114    pub const FORWARDABLE: u32 = bit(1);
115    pub const FORWARDED: u32 = bit(2);
116    pub const PROXIABLE: u32 = bit(3);
117    pub const PROXY: u32 = bit(4);
118    pub const ALLOW_POSTDATE: u32 = bit(5);
119    pub const POSTDATED: u32 = bit(6);
120    pub const RENEWABLE: u32 = bit(8);
121    pub const CANONICALIZE: u32 = bit(15);
122    pub const DISABLE_TRANSITED_CHECK: u32 = bit(26);
123    pub const RENEWABLE_OK: u32 = bit(27);
124    pub const ENC_TKT_IN_SKEY: u32 = bit(28);
125    pub const RENEW: u32 = bit(30);
126    pub const VALIDATE: u32 = bit(31);
127
128    /// Options this KDC does not implement. A request carrying one is refused with
129    /// KDC_ERR_BADOPTION rather than half-honoured.
130    pub const UNSUPPORTED: u32 =
131        FORWARDED | PROXY | ALLOW_POSTDATE | POSTDATED | ENC_TKT_IN_SKEY | RENEW | VALIDATE;
132
133    pub const NAMES: &[(u32, &str)] = &[
134        (FORWARDABLE, "forwardable"),
135        (FORWARDED, "forwarded"),
136        (PROXIABLE, "proxiable"),
137        (PROXY, "proxy"),
138        (ALLOW_POSTDATE, "allow_postdate"),
139        (POSTDATED, "postdated"),
140        (RENEWABLE, "renewable"),
141        (CANONICALIZE, "canonicalize"),
142        (DISABLE_TRANSITED_CHECK, "disable_transited_check"),
143        (RENEWABLE_OK, "renewable_ok"),
144        (ENC_TKT_IN_SKEY, "enc_tkt_in_skey"),
145        (RENEW, "renew"),
146        (VALIDATE, "validate"),
147    ];
148}
149
150/// Ticket flags (RFC 4120 §5.3).
151pub mod ticket_flags {
152    use super::bit;
153    pub const FORWARDABLE: u32 = bit(1);
154    pub const FORWARDED: u32 = bit(2);
155    pub const PROXIABLE: u32 = bit(3);
156    pub const PROXY: u32 = bit(4);
157    pub const MAY_POSTDATE: u32 = bit(5);
158    pub const POSTDATED: u32 = bit(6);
159    pub const INVALID: u32 = bit(7);
160    pub const RENEWABLE: u32 = bit(8);
161    pub const INITIAL: u32 = bit(9);
162    pub const PRE_AUTHENT: u32 = bit(10);
163    pub const HW_AUTHENT: u32 = bit(11);
164    pub const TRANSITED_POLICY_CHECKED: u32 = bit(12);
165    pub const OK_AS_DELEGATE: u32 = bit(13);
166    pub const ENC_PA_REP: u32 = bit(15);
167
168    pub const NAMES: &[(u32, &str)] = &[
169        (FORWARDABLE, "forwardable"),
170        (FORWARDED, "forwarded"),
171        (PROXIABLE, "proxiable"),
172        (PROXY, "proxy"),
173        (MAY_POSTDATE, "may_postdate"),
174        (POSTDATED, "postdated"),
175        (INVALID, "invalid"),
176        (RENEWABLE, "renewable"),
177        (INITIAL, "initial"),
178        (PRE_AUTHENT, "pre_authent"),
179        (HW_AUTHENT, "hw_authent"),
180        (TRANSITED_POLICY_CHECKED, "transited_policy_checked"),
181        (OK_AS_DELEGATE, "ok_as_delegate"),
182        (ENC_PA_REP, "enc_pa_rep"),
183    ];
184}
185
186/// The names of the bits set in `value`, from a `(bit, name)` table.
187pub fn flag_names(value: u32, table: &[(u32, &str)]) -> Vec<String> {
188    table
189        .iter()
190        .filter(|(b, _)| value & b != 0)
191        .map(|(_, n)| n.to_string())
192        .collect()
193}
194
195/// The first 32 bits of a KerberosFlags BIT STRING as an integer. Shorter strings are padded
196/// with zero bits; anything past bit 31 is ignored (RFC 4120 §5.2.8 allows it and assigns none).
197pub fn flags_u32(flags: &KerberosFlags) -> u32 {
198    let data = flags.0.as_bytes();
199    let payload = data.get(1..).unwrap_or(&[]);
200    let mut out = [0u8; 4];
201    for (i, b) in payload.iter().take(4).enumerate() {
202        out[i] = *b;
203    }
204    u32::from_be_bytes(out)
205}
206
207/// A 32-bit KerberosFlags BIT STRING.
208pub fn flags(value: u32) -> KerberosFlags {
209    BitStringAsn1(BitString::with_bytes(value.to_be_bytes().to_vec()))
210}
211
212// ---------------------------------------------------------------------------------------------
213// The bounded DER walk.
214// ---------------------------------------------------------------------------------------------
215
216/// Why a buffer was refused before any typed decoding.
217#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
218pub enum DerError {
219    #[error("empty input")]
220    Empty,
221    #[error("message is {0} bytes; the limit is {MAX_MESSAGE_LEN}")]
222    TooLarge(usize),
223    #[error("truncated element at offset {0}")]
224    Truncated(usize),
225    #[error("indefinite length at offset {0}; DER requires definite lengths")]
226    Indefinite(usize),
227    #[error("multi-byte tag at offset {0}; Kerberos uses none")]
228    LongTag(usize),
229    #[error("length at offset {0} is wider than four octets")]
230    LengthTooWide(usize),
231    #[error("element at offset {0} runs past its enclosing element")]
232    Overrun(usize),
233    #[error("nesting deeper than {MAX_DER_DEPTH} at offset {0}")]
234    TooDeep(usize),
235    #[error("more than {MAX_DER_ELEMENTS} elements")]
236    TooManyElements,
237    #[error("{0} trailing bytes after the message")]
238    Trailing(usize),
239}
240
241/// One element's header: where its content starts and how long it is.
242struct Header {
243    tag: u8,
244    content_start: usize,
245    content_len: usize,
246}
247
248/// Read the header at `pos`, bounded by `limit` (the end of the enclosing element).
249fn header_at(bytes: &[u8], pos: usize, limit: usize) -> Result<Header, DerError> {
250    let tag = *bytes.get(pos).ok_or(DerError::Truncated(pos))?;
251    if tag & 0x1f == 0x1f {
252        return Err(DerError::LongTag(pos));
253    }
254    let first = *bytes.get(pos + 1).ok_or(DerError::Truncated(pos))?;
255    let (content_len, header_len) = if first < 0x80 {
256        (first as usize, 2)
257    } else if first == 0x80 {
258        return Err(DerError::Indefinite(pos));
259    } else {
260        let n = (first & 0x7f) as usize;
261        if n > 4 {
262            return Err(DerError::LengthTooWide(pos));
263        }
264        let start = pos + 2;
265        let end = start.checked_add(n).ok_or(DerError::Truncated(pos))?;
266        let octets = bytes.get(start..end).ok_or(DerError::Truncated(pos))?;
267        let len = octets
268            .iter()
269            .fold(0usize, |acc, b| (acc << 8) | *b as usize);
270        (len, 2 + n)
271    };
272    let content_start = pos + header_len;
273    let end = content_start
274        .checked_add(content_len)
275        .ok_or(DerError::Overrun(pos))?;
276    if end > limit || end > bytes.len() {
277        return Err(DerError::Overrun(pos));
278    }
279    Ok(Header {
280        tag,
281        content_start,
282        content_len,
283    })
284}
285
286/// Validate `bytes` as exactly one bounded DER element. Iterative: the only memory it uses is
287/// a stack of at most [`MAX_DER_DEPTH`] end offsets.
288pub fn check_der(bytes: &[u8]) -> Result<(), DerError> {
289    if bytes.is_empty() {
290        return Err(DerError::Empty);
291    }
292    if bytes.len() > MAX_MESSAGE_LEN {
293        return Err(DerError::TooLarge(bytes.len()));
294    }
295    let top = header_at(bytes, 0, bytes.len())?;
296    let top_end = top.content_start + top.content_len;
297    if top_end != bytes.len() {
298        return Err(DerError::Trailing(bytes.len() - top_end));
299    }
300    // `ends` holds the end offset of every constructed element we are inside.
301    let mut ends: Vec<usize> = Vec::with_capacity(MAX_DER_DEPTH);
302    let mut elements = 1usize;
303    let mut pos;
304    if top.tag & 0x20 != 0 {
305        ends.push(top_end);
306        pos = top.content_start;
307    } else {
308        return Ok(());
309    }
310    while let Some(&end) = ends.last() {
311        if pos == end {
312            ends.pop();
313            continue;
314        }
315        let h = header_at(bytes, pos, end)?;
316        elements += 1;
317        if elements > MAX_DER_ELEMENTS {
318            return Err(DerError::TooManyElements);
319        }
320        let element_end = h.content_start + h.content_len;
321        if h.tag & 0x20 != 0 {
322            if ends.len() >= MAX_DER_DEPTH {
323                return Err(DerError::TooDeep(pos));
324            }
325            ends.push(element_end);
326            pos = h.content_start;
327        } else {
328            pos = element_end;
329        }
330    }
331    Ok(())
332}
333
334/// Decode a bounded DER message into a `picky-krb` type.
335///
336/// [`check_der`] runs first, so the serde decoder only ever sees a buffer whose nesting and
337/// lengths are already bounded. A panic inside the decoder is caught and reported as a refusal:
338/// a peer must not be able to take a task down by sending bytes the library did not expect.
339pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
340    check_der(bytes).map_err(|e| anyhow!("refused before decoding: {e}"))?;
341    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
342        picky_asn1_der::from_bytes::<T>(bytes)
343    }));
344    match result {
345        Ok(Ok(v)) => Ok(v),
346        Ok(Err(e)) => Err(anyhow!("DER decode failed: {e}")),
347        Err(_) => Err(anyhow!("DER decoder panicked; refused")),
348    }
349}
350
351/// Encode a `picky-krb` value.
352pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>> {
353    picky_asn1_der::to_vec(value).map_err(|e| anyhow!("DER encode failed: {e}"))
354}
355
356/// The raw DER of a KDC-REQ's `req-body` ([4] KDC-REQ-BODY), exactly as the peer sent it. The
357/// TGS authenticator's checksum is computed over these bytes, so they must not be re-encoded.
358pub fn req_body_bytes(message: &[u8]) -> Option<&[u8]> {
359    check_der(message).ok()?;
360    let app = header_at(message, 0, message.len()).ok()?;
361    let seq = header_at(message, app.content_start, message.len()).ok()?;
362    if seq.tag != 0x30 {
363        return None;
364    }
365    let end = seq.content_start + seq.content_len;
366    let mut pos = seq.content_start;
367    while pos < end {
368        let field = header_at(message, pos, end).ok()?;
369        let field_end = field.content_start + field.content_len;
370        if field.tag == 0xa4 {
371            let body = header_at(message, field.content_start, field_end).ok()?;
372            let body_end = body.content_start + body.content_len;
373            if body.tag != 0x30 || body_end != field_end {
374                return None;
375            }
376            return message.get(field.content_start..body_end);
377        }
378        pos = field_end;
379    }
380    None
381}
382
383// ---------------------------------------------------------------------------------------------
384// Integers, strings, times.
385// ---------------------------------------------------------------------------------------------
386
387/// A signed INTEGER as `i64`, or `None` when it does not fit.
388pub fn int_i64(value: &IntegerAsn1) -> Option<i64> {
389    let bytes = value.as_signed_bytes_be();
390    if bytes.is_empty() || bytes.len() > 8 {
391        return None;
392    }
393    let fill = if bytes[0] & 0x80 != 0 { 0xff } else { 0 };
394    let mut buf = [fill; 8];
395    buf[8 - bytes.len()..].copy_from_slice(bytes);
396    Some(i64::from_be_bytes(buf))
397}
398
399/// `value` as a minimal signed DER INTEGER.
400pub fn int(value: i64) -> IntegerAsn1 {
401    IntegerAsn1::from_bytes_be_signed(value.to_be_bytes().to_vec())
402}
403
404/// A KerberosString from text. KerberosString is IA5 in practice (RFC 4120 §5.2.1).
405pub fn kstring(s: &str) -> Result<KerberosStringAsn1> {
406    Ok(GeneralStringAsn1(
407        Ia5String::from_string(s.to_string())
408            .map_err(|_| anyhow!("'{s}' is not an IA5 (ASCII) string"))?,
409    ))
410}
411
412/// The text of a KerberosString.
413pub fn kstring_text(s: &KerberosStringAsn1) -> String {
414    s.0.as_utf8().to_string()
415}
416
417/// Seconds since the Unix epoch, now.
418pub fn now_secs() -> i64 {
419    crate::utils::clock::SystemTime::now()
420        .duration_since(crate::utils::clock::UNIX_EPOCH)
421        .map(|d| d.as_secs() as i64)
422        .unwrap_or(0)
423}
424
425/// A KerberosTime (`YYYYMMDDHHMMSSZ`) for `secs` since the epoch, clamped to what
426/// GeneralizedTime can hold here.
427pub fn ktime(secs: i64) -> KerberosTime {
428    let secs = secs.clamp(0, 253_402_300_799); // 9999-12-31T23:59:59Z
429    let dt = chrono::DateTime::from_timestamp(secs, 0).unwrap_or_default();
430    use chrono::{Datelike, Timelike};
431    let date = Date::new(
432        dt.year() as u16,
433        dt.month() as u8,
434        dt.day() as u8,
435        dt.hour() as u8,
436        dt.minute() as u8,
437        dt.second() as u8,
438    )
439    .unwrap_or_else(|| Date::new(1970, 1, 1, 0, 0, 0).expect("the epoch is a valid date"));
440    GeneralizedTimeAsn1(date)
441}
442
443/// Seconds since the epoch of a KerberosTime, or `None` for a date that does not exist.
444pub fn unix(t: &KerberosTime) -> Option<i64> {
445    let d = &t.0;
446    let date = chrono::NaiveDate::from_ymd_opt(d.year() as i32, d.month() as u32, d.day() as u32)?;
447    let time = date.and_hms_opt(d.hour() as u32, d.minute() as u32, d.second() as u32)?;
448    Some(time.and_utc().timestamp())
449}
450
451/// RFC 3339 text for event payloads.
452pub fn rfc3339(secs: i64) -> String {
453    chrono::DateTime::from_timestamp(secs, 0)
454        .map(|d| d.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
455        .unwrap_or_default()
456}
457
458// ---------------------------------------------------------------------------------------------
459// Principal names.
460// ---------------------------------------------------------------------------------------------
461
462/// A principal name without its realm: a name type and one or more components.
463#[derive(Clone, PartialEq, Eq, Hash)]
464pub struct Principal {
465    pub name_type: i32,
466    pub components: Vec<String>,
467}
468
469impl std::fmt::Display for Principal {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        f.write_str(&self.components.join("/"))
472    }
473}
474
475impl std::fmt::Debug for Principal {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        write!(f, "Principal({self})")
478    }
479}
480
481/// Refuse a name component or realm a peer sent that cannot be shown or compared safely.
482fn check_component(s: &str, what: &str) -> Result<()> {
483    if s.is_empty() {
484        return Err(anyhow!("empty {what}"));
485    }
486    if s.len() > MAX_NAME_LEN {
487        return Err(anyhow!(
488            "{what} is {} bytes; the limit is {MAX_NAME_LEN}",
489            s.len()
490        ));
491    }
492    if crate::utils::sanitize::has_controls(s) {
493        return Err(anyhow!("{what} contains a control character"));
494    }
495    if s.contains('/') || s.contains('@') || s.contains('\\') {
496        return Err(anyhow!("{what} contains '/', '@' or '\\'"));
497    }
498    Ok(())
499}
500
501/// Refuse a realm a peer sent or the operator configured.
502pub fn check_realm(realm: &str) -> Result<()> {
503    if realm.is_empty() || realm.len() > MAX_NAME_LEN {
504        return Err(anyhow!("realm must be 1-{MAX_NAME_LEN} bytes"));
505    }
506    if !realm.is_ascii() || crate::utils::sanitize::has_controls(realm) {
507        return Err(anyhow!("realm must be printable ASCII"));
508    }
509    if realm.contains('/') || realm.contains('@') || realm.contains('\\') {
510        return Err(anyhow!("realm contains '/', '@' or '\\'"));
511    }
512    Ok(())
513}
514
515impl Principal {
516    /// `krbtgt/REALM`, the ticket-granting service.
517    pub fn krbtgt(realm: &str) -> Self {
518        Self {
519            name_type: NT_SRV_INST,
520            components: vec!["krbtgt".to_string(), realm.to_string()],
521        }
522    }
523
524    /// Parse `a/b` or `a/b@REALM`. Returns the principal and the realm it named, if any.
525    pub fn parse(text: &str) -> Result<(Self, Option<String>)> {
526        let (name, realm) = match text.rsplit_once('@') {
527            Some((n, r)) => {
528                check_realm(r)?;
529                (n, Some(r.to_string()))
530            }
531            None => (text, None),
532        };
533        let components: Vec<String> = name.split('/').map(str::to_string).collect();
534        if components.len() > MAX_NAME_COMPONENTS {
535            return Err(anyhow!(
536                "'{name}' has more than {MAX_NAME_COMPONENTS} components"
537            ));
538        }
539        for c in &components {
540            check_component(c, "principal name component")?;
541        }
542        let name_type = if components.len() > 1 {
543            NT_SRV_INST
544        } else {
545            NT_PRINCIPAL
546        };
547        Ok((
548            Self {
549                name_type,
550                components,
551            },
552            realm,
553        ))
554    }
555
556    /// Read a PrincipalName a peer sent, refusing anything unsafe to show or compare.
557    pub fn from_asn1(name: &PrincipalName) -> Result<Self> {
558        let name_type = int_i64(&name.name_type.0)
559            .and_then(|v| i32::try_from(v).ok())
560            .context("name-type out of range")?;
561        let parts = &name.name_string.0 .0;
562        if parts.is_empty() || parts.len() > MAX_NAME_COMPONENTS {
563            return Err(anyhow!(
564                "a principal name needs 1-{MAX_NAME_COMPONENTS} components, not {}",
565                parts.len()
566            ));
567        }
568        let mut components = Vec::with_capacity(parts.len());
569        for p in parts {
570            let s = kstring_text(p);
571            check_component(&s, "principal name component")?;
572            components.push(s);
573        }
574        Ok(Self {
575            name_type,
576            components,
577        })
578    }
579
580    pub fn to_asn1(&self) -> Result<PrincipalName> {
581        let mut parts = Vec::with_capacity(self.components.len());
582        for c in &self.components {
583            parts.push(kstring(c)?);
584        }
585        Ok(PrincipalName {
586            name_type: ExplicitContextTag0(int(self.name_type as i64)),
587            name_string: ExplicitContextTag1(Asn1SequenceOf(parts)),
588        })
589    }
590
591    /// Same name, ignoring the name type (RFC 4120 §6.2: the type is a hint, not part of the
592    /// name).
593    pub fn same_name(&self, other: &Principal) -> bool {
594        self.components == other.components
595    }
596
597    pub fn is_krbtgt(&self, realm: &str) -> bool {
598        self.components.len() == 2 && self.components[0] == "krbtgt" && self.components[1] == realm
599    }
600
601    /// The default salt for this principal in `realm`: the realm followed by every component
602    /// (RFC 3961 §4 / RFC 4120 §4).
603    pub fn default_salt(&self, realm: &str) -> String {
604        let mut s = realm.to_string();
605        for c in &self.components {
606            s.push_str(c);
607        }
608        s
609    }
610}
611
612// ---------------------------------------------------------------------------------------------
613// Enctypes and keys.
614// ---------------------------------------------------------------------------------------------
615
616/// The registered name of an enctype, for events.
617pub fn etype_name(etype: i32) -> String {
618    match etype {
619        1 => "des-cbc-crc".into(),
620        3 => "des-cbc-md5".into(),
621        16 => "des3-cbc-sha1".into(),
622        17 => "aes128-cts-hmac-sha1-96".into(),
623        18 => "aes256-cts-hmac-sha1-96".into(),
624        19 => "aes128-cts-hmac-sha256-128".into(),
625        20 => "aes256-cts-hmac-sha384-192".into(),
626        23 => "arcfour-hmac".into(),
627        24 => "arcfour-hmac-exp".into(),
628        25 => "camellia128-cts-cmac".into(),
629        26 => "camellia256-cts-cmac".into(),
630        other => format!("etype-{other}"),
631    }
632}
633
634fn suite(etype: i32) -> Result<CipherSuite> {
635    match etype {
636        ETYPE_AES256 => Ok(CipherSuite::Aes256CtsHmacSha196),
637        ETYPE_AES128 => Ok(CipherSuite::Aes128CtsHmacSha196),
638        other => Err(anyhow!("enctype {} is not supported", etype_name(other))),
639    }
640}
641
642fn key_len(etype: i32) -> usize {
643    if etype == ETYPE_AES256 {
644        32
645    } else {
646        16
647    }
648}
649
650/// Why a decryption failed. Distinguished because the KDC answers them differently.
651#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
652pub enum CryptoError {
653    #[error("unsupported enctype {0}")]
654    Etype(i32),
655    #[error("ciphertext is too short")]
656    Length,
657    #[error("integrity check failed")]
658    Integrity,
659    #[error("cipher error")]
660    Cipher,
661}
662
663/// A Kerberos key. Not `Debug`, not `Display`, not `Serialize`: the bytes leave this type only
664/// as a ciphertext, a checksum, or the EncryptionKey inside an encrypted part.
665#[derive(Clone)]
666pub struct Key {
667    etype: i32,
668    bytes: Vec<u8>,
669}
670
671impl Key {
672    /// Derive a key from a password with PBKDF2-HMAC-SHA1, 4096 iterations (RFC 3962 §4's
673    /// default, and what both MIT and Heimdal use when no s2kparams are sent).
674    pub fn from_password(etype: i32, password: &str, salt: &str) -> Result<Self> {
675        let bytes = suite(etype)?
676            .cipher()
677            .generate_key_from_password(password.as_bytes(), salt.as_bytes())
678            .map_err(|_| anyhow!("string-to-key failed for {}", etype_name(etype)))?;
679        Ok(Self { etype, bytes })
680    }
681
682    /// A fresh random key from the operating system's generator.
683    pub fn random(etype: i32) -> Result<Self> {
684        suite(etype)?;
685        let mut bytes = vec![0u8; key_len(etype)];
686        use rand::RngCore;
687        rand::rngs::OsRng.fill_bytes(&mut bytes);
688        Ok(Self { etype, bytes })
689    }
690
691    /// A key a peer delivered inside an encrypted part (a session key or a subkey).
692    pub fn from_wire(etype: i32, bytes: Vec<u8>) -> Result<Self> {
693        suite(etype)?;
694        if bytes.len() != key_len(etype) {
695            return Err(anyhow!(
696                "a {} key must be {} bytes",
697                etype_name(etype),
698                key_len(etype)
699            ));
700        }
701        Ok(Self { etype, bytes })
702    }
703
704    pub fn etype(&self) -> i32 {
705        self.etype
706    }
707
708    /// The EncryptionKey structure carrying this key, for an encrypted part only.
709    pub fn to_asn1(&self) -> picky_krb::data_types::EncryptionKey {
710        picky_krb::data_types::EncryptionKey {
711            key_type: ExplicitContextTag0(int(self.etype as i64)),
712            key_value: ExplicitContextTag1(OctetStringAsn1(self.bytes.clone())),
713        }
714    }
715
716    /// Read an EncryptionKey out of a decrypted part.
717    pub fn from_asn1(key: &picky_krb::data_types::EncryptionKey) -> Result<Self> {
718        let etype = int_i64(&key.key_type.0)
719            .and_then(|v| i32::try_from(v).ok())
720            .context("key type out of range")?;
721        Self::from_wire(etype, key.key_value.0 .0.clone())
722    }
723
724    /// The raw key bytes, for the client's credential cache file only.
725    pub fn bytes_for_ccache(&self) -> &[u8] {
726        &self.bytes
727    }
728
729    /// Encrypt `plaintext` under `usage` with a fresh random confounder.
730    pub fn encrypt(&self, usage: i32, plaintext: &[u8]) -> Result<Vec<u8>> {
731        suite(self.etype)?
732            .cipher()
733            .encrypt(&self.bytes, usage, plaintext)
734            .map_err(|_| anyhow!("encryption failed"))
735    }
736
737    /// Encrypt into an EncryptedData with this key's enctype.
738    pub fn encrypted_data(
739        &self,
740        usage: i32,
741        plaintext: &[u8],
742        kvno: Option<i64>,
743    ) -> Result<EncryptedData> {
744        Ok(EncryptedData {
745            etype: ExplicitContextTag0(int(self.etype as i64)),
746            kvno: Optional(kvno.map(|k| ExplicitContextTag1(int(k)))),
747            cipher: ExplicitContextTag2(OctetStringAsn1(self.encrypt(usage, plaintext)?)),
748        })
749    }
750
751    /// Decrypt and verify. The HMAC-SHA1-96 over confounder and plaintext is recomputed here
752    /// and compared in constant time; nothing is returned unless it matches.
753    pub fn decrypt(&self, usage: i32, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
754        let cipher = suite(self.etype)
755            .map_err(|_| CryptoError::Etype(self.etype))?
756            .cipher();
757        if ciphertext.len() < 16 + 12 {
758            return Err(CryptoError::Length);
759        }
760        let parts = cipher
761            .decrypt_no_checksum(&self.bytes, usage, ciphertext)
762            .map_err(|_| CryptoError::Cipher)?;
763        let mut mac =
764            Hmac::<sha1::Sha1>::new_from_slice(&parts.ki).map_err(|_| CryptoError::Cipher)?;
765        mac.update(&parts.confounder);
766        mac.update(&parts.plaintext);
767        let full = mac.finalize().into_bytes();
768        if !constant_time_eq(&full[..12], &parts.checksum) {
769            return Err(CryptoError::Integrity);
770        }
771        Ok(parts.plaintext)
772    }
773
774    /// Decrypt an EncryptedData, refusing one whose enctype is not this key's.
775    pub fn decrypt_data(&self, usage: i32, data: &EncryptedData) -> Result<Vec<u8>, CryptoError> {
776        let etype = int_i64(&data.etype.0).unwrap_or(-1);
777        if etype != self.etype as i64 {
778            return Err(CryptoError::Etype(etype as i32));
779        }
780        self.decrypt(usage, &data.cipher.0 .0)
781    }
782
783    /// The keyed checksum type that goes with this key's enctype.
784    pub fn checksum_type(&self) -> i32 {
785        if self.etype == ETYPE_AES256 {
786            CKSUM_HMAC_SHA1_96_AES256
787        } else {
788            CKSUM_HMAC_SHA1_96_AES128
789        }
790    }
791
792    /// The keyed checksum of `data` under `usage`: HMAC-SHA1-96 with the checksum key
793    /// Kc = DK(key, usage | 0x99) (RFC 3961 §5.3, RFC 3962). Not the encryption integrity key
794    /// Ki (usage | 0x55): the two differ, and only Heimdal's TGS-REQ showed it, since NetGet's
795    /// client and KDC agreed with each other on the wrong one.
796    pub fn checksum(&self, usage: i32, data: &[u8]) -> Result<Vec<u8>> {
797        let suite = if self.etype == ETYPE_AES256 {
798            ChecksumSuite::HmacSha196Aes256
799        } else {
800            ChecksumSuite::HmacSha196Aes128
801        };
802        suite
803            .hasher()
804            .checksum(&self.bytes, usage, data)
805            .map_err(|_| anyhow!("checksum failed"))
806    }
807
808    /// Verify a keyed checksum. Only this key's own checksum type is accepted: an unkeyed or
809    /// foreign checksum type would let a peer that does not hold the key forge one.
810    pub fn verify_checksum(
811        &self,
812        cksumtype: i32,
813        usage: i32,
814        data: &[u8],
815        expected: &[u8],
816    ) -> bool {
817        if cksumtype != self.checksum_type() {
818            return false;
819        }
820        match self.checksum(usage, data) {
821            Ok(sum) => constant_time_eq(&sum, expected),
822            Err(_) => false,
823        }
824    }
825}
826
827/// Compare two byte strings without an early exit on the first difference.
828pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
829    if a.len() != b.len() {
830        return false;
831    }
832    a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
833}
834
835// ---------------------------------------------------------------------------------------------
836// PA-DATA helpers.
837// ---------------------------------------------------------------------------------------------
838
839pub fn padata(padata_type: i32, value: Vec<u8>) -> PaData {
840    PaData {
841        padata_type: ExplicitContextTag1(int(padata_type as i64)),
842        padata_data: ExplicitContextTag2(OctetStringAsn1(value)),
843    }
844}
845
846pub fn padata_type(p: &PaData) -> i64 {
847    int_i64(&p.padata_type.0).unwrap_or(-1)
848}
849
850/// The DER of an ETYPE-INFO2 (RFC 4120 §5.2.7.5) naming each `(etype, salt)`.
851pub fn etype_info2(entries: &[(i32, String)]) -> Result<Vec<u8>> {
852    let mut list = Vec::with_capacity(entries.len());
853    for (etype, salt) in entries {
854        list.push(EtypeInfo2Entry {
855            etype: ExplicitContextTag0(int(*etype as i64)),
856            salt: Optional(Some(ExplicitContextTag1(kstring(salt)?))),
857            s2kparams: Optional(None),
858        });
859    }
860    encode(&Asn1SequenceOf(list))
861}
862
863// ---------------------------------------------------------------------------------------------
864// Error codes (RFC 4120 §7.5.9).
865// ---------------------------------------------------------------------------------------------
866
867pub const KDC_ERR_C_PRINCIPAL_UNKNOWN: i32 = 6;
868pub const KDC_ERR_S_PRINCIPAL_UNKNOWN: i32 = 7;
869pub const KDC_ERR_NEVER_VALID: i32 = 11;
870pub const KDC_ERR_POLICY: i32 = 12;
871pub const KDC_ERR_BADOPTION: i32 = 13;
872pub const KDC_ERR_ETYPE_NOSUPP: i32 = 14;
873pub const KDC_ERR_PADATA_TYPE_NOSUPP: i32 = 16;
874pub const KDC_ERR_PREAUTH_FAILED: i32 = 24;
875pub const KDC_ERR_PREAUTH_REQUIRED: i32 = 25;
876pub const KDC_ERR_SVC_UNAVAILABLE: i32 = 29;
877pub const KRB_AP_ERR_BAD_INTEGRITY: i32 = 31;
878pub const KRB_AP_ERR_TKT_EXPIRED: i32 = 32;
879pub const KRB_AP_ERR_TKT_NYV: i32 = 33;
880pub const KRB_AP_ERR_REPEAT: i32 = 34;
881pub const KRB_AP_ERR_BADMATCH: i32 = 36;
882pub const KRB_AP_ERR_SKEW: i32 = 37;
883pub const KRB_AP_ERR_MSG_TYPE: i32 = 40;
884pub const KRB_AP_ERR_MODIFIED: i32 = 41;
885pub const KRB_AP_ERR_INAPP_CKSUM: i32 = 50;
886pub const KRB_ERR_RESPONSE_TOO_BIG: i32 = 52;
887pub const KRB_ERR_GENERIC: i32 = 60;
888pub const KRB_ERR_FIELD_TOOLONG: i32 = 61;
889pub const KDC_ERR_WRONG_REALM: i32 = 68;
890
891/// Every error code with its RFC name.
892pub const ERROR_NAMES: &[(i32, &str)] = &[
893    (0, "KDC_ERR_NONE"),
894    (1, "KDC_ERR_NAME_EXP"),
895    (2, "KDC_ERR_SERVICE_EXP"),
896    (3, "KDC_ERR_BAD_PVNO"),
897    (4, "KDC_ERR_C_OLD_MAST_KVNO"),
898    (5, "KDC_ERR_S_OLD_MAST_KVNO"),
899    (6, "KDC_ERR_C_PRINCIPAL_UNKNOWN"),
900    (7, "KDC_ERR_S_PRINCIPAL_UNKNOWN"),
901    (8, "KDC_ERR_PRINCIPAL_NOT_UNIQUE"),
902    (9, "KDC_ERR_NULL_KEY"),
903    (10, "KDC_ERR_CANNOT_POSTDATE"),
904    (11, "KDC_ERR_NEVER_VALID"),
905    (12, "KDC_ERR_POLICY"),
906    (13, "KDC_ERR_BADOPTION"),
907    (14, "KDC_ERR_ETYPE_NOSUPP"),
908    (15, "KDC_ERR_SUMTYPE_NOSUPP"),
909    (16, "KDC_ERR_PADATA_TYPE_NOSUPP"),
910    (17, "KDC_ERR_TRTYPE_NOSUPP"),
911    (18, "KDC_ERR_CLIENT_REVOKED"),
912    (19, "KDC_ERR_SERVICE_REVOKED"),
913    (20, "KDC_ERR_TGT_REVOKED"),
914    (21, "KDC_ERR_CLIENT_NOTYET"),
915    (22, "KDC_ERR_SERVICE_NOTYET"),
916    (23, "KDC_ERR_KEY_EXPIRED"),
917    (24, "KDC_ERR_PREAUTH_FAILED"),
918    (25, "KDC_ERR_PREAUTH_REQUIRED"),
919    (26, "KDC_ERR_SERVER_NOMATCH"),
920    (27, "KDC_ERR_MUST_USE_USER2USER"),
921    (28, "KDC_ERR_PATH_NOT_ACCEPTED"),
922    (29, "KDC_ERR_SVC_UNAVAILABLE"),
923    (31, "KRB_AP_ERR_BAD_INTEGRITY"),
924    (32, "KRB_AP_ERR_TKT_EXPIRED"),
925    (33, "KRB_AP_ERR_TKT_NYV"),
926    (34, "KRB_AP_ERR_REPEAT"),
927    (35, "KRB_AP_ERR_NOT_US"),
928    (36, "KRB_AP_ERR_BADMATCH"),
929    (37, "KRB_AP_ERR_SKEW"),
930    (38, "KRB_AP_ERR_BADADDR"),
931    (39, "KRB_AP_ERR_BADVERSION"),
932    (40, "KRB_AP_ERR_MSG_TYPE"),
933    (41, "KRB_AP_ERR_MODIFIED"),
934    (42, "KRB_AP_ERR_BADORDER"),
935    (44, "KRB_AP_ERR_BADKEYVER"),
936    (45, "KRB_AP_ERR_NOKEY"),
937    (46, "KRB_AP_ERR_MUT_FAIL"),
938    (47, "KRB_AP_ERR_BADDIRECTION"),
939    (48, "KRB_AP_ERR_METHOD"),
940    (49, "KRB_AP_ERR_BADSEQ"),
941    (50, "KRB_AP_ERR_INAPP_CKSUM"),
942    (51, "KRB_AP_PATH_NOT_ACCEPTED"),
943    (52, "KRB_ERR_RESPONSE_TOO_BIG"),
944    (60, "KRB_ERR_GENERIC"),
945    (61, "KRB_ERR_FIELD_TOOLONG"),
946    (68, "KDC_ERR_WRONG_REALM"),
947];
948
949/// The RFC name of an error code.
950pub fn error_name(code: i32) -> String {
951    ERROR_NAMES
952        .iter()
953        .find(|(c, _)| *c == code)
954        .map(|(_, n)| n.to_string())
955        .unwrap_or_else(|| format!("KRB_ERROR_{code}"))
956}
957
958/// The refusals the model may choose. KRB_ERR_GENERIC is absent on purpose: it is what the
959/// server sends when no decision was produced, and keeping it out of the model's vocabulary
960/// keeps "the model refused" and "nobody decided" apart on the wire. KDC_ERR_SVC_UNAVAILABLE is
961/// absent too: Heimdal 7.8 clients retry it forever against a single KDC (see `mod.rs::decide`).
962pub const MODEL_REFUSALS: &[i32] = &[
963    1, 2, 6, 7, 10, 11, 12, 13, 14, 18, 19, 20, 21, 22, 23, 24, 27,
964];
965
966/// Resolve a refusal the model named — `C_PRINCIPAL_UNKNOWN`, `KDC_ERR_POLICY`, `policy` or the
967/// number — to a code in [`MODEL_REFUSALS`].
968pub fn model_refusal_code(value: &serde_json::Value) -> Result<i32> {
969    let code = if let Some(n) = value.as_i64() {
970        i32::try_from(n).ok()
971    } else if let Some(s) = value.as_str() {
972        let wanted = s.trim().to_ascii_uppercase().replace(['-', ' '], "_");
973        ERROR_NAMES
974            .iter()
975            .find(|(_, name)| {
976                *name == wanted
977                    || name.strip_prefix("KDC_ERR_") == Some(wanted.as_str())
978                    || name.strip_prefix("KRB_AP_ERR_") == Some(wanted.as_str())
979            })
980            .map(|(c, _)| *c)
981    } else {
982        None
983    };
984    match code {
985        Some(c) if MODEL_REFUSALS.contains(&c) => Ok(c),
986        _ => Err(anyhow!(
987            "'error' must be one of: {}",
988            MODEL_REFUSALS
989                .iter()
990                .map(|c| error_name(*c).trim_start_matches("KDC_ERR_").to_string())
991                .collect::<Vec<_>>()
992                .join(", ")
993        )),
994    }
995}