Skip to main content

netget/client/kerberos/
wire.rs

1//! What a Kerberos client builds and checks: AS-REQ (with or without PA-ENC-TIMESTAMP),
2//! TGS-REQ (AP-REQ with an authenticator whose keyed checksum covers the request body, and a
3//! subkey), and the verification of every reply before anything in it is believed.
4//!
5//! The KDC's codec (`src/server/kerberos/codec.rs`) supplies the bounded DER walk, the keys and
6//! the vocabulary; nothing here is a second copy of it.
7
8use anyhow::{anyhow, Context, Result};
9use picky_asn1::wrapper::{
10    Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2,
11    ExplicitContextTag3, ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6,
12    ExplicitContextTag7, ExplicitContextTag8, ExplicitContextTag9, OctetStringAsn1, Optional,
13};
14use picky_krb::data_types::{
15    Authenticator, AuthenticatorInner, Checksum, EtypeInfo2, HostAddress, PaData, PaEncTsEnc,
16    Ticket,
17};
18use picky_krb::messages::{
19    ApReq, ApReqInner, AsRep, AsReq, EncAsRepPart, EncKdcRepPart, EncTgsRepPart, KdcRep, KdcReq,
20    KdcReqBody, KrbError, KrbErrorInner, TgsRep, TgsReq,
21};
22
23use crate::server::kerberos::codec::{
24    self, decode, encode, flags, flags_u32, int, int_i64, kstring, kstring_text, now_secs, unix,
25    Key, Principal, PA_ENC_TIMESTAMP, PA_ETYPE_INFO2, PA_TGS_REQ, SUPPORTED_ETYPES,
26};
27
28/// A ticket and its session key, as a client holds them. Not `Debug`: it carries the key.
29#[derive(Clone)]
30pub struct Credential {
31    pub client: Principal,
32    pub client_realm: String,
33    pub service: Principal,
34    pub service_realm: String,
35    pub ticket: Ticket,
36    pub session_key: Key,
37    pub flags: u32,
38    pub authtime: i64,
39    pub starttime: i64,
40    pub endtime: i64,
41    pub renew_till: Option<i64>,
42}
43
44impl Credential {
45    /// What the model is told about a credential. Never the key.
46    pub fn summary(&self) -> serde_json::Value {
47        let now = now_secs();
48        let ticket_etype = int_i64(&self.ticket.0.enc_part.0.etype.0).unwrap_or(-1) as i32;
49        serde_json::json!({
50            "client": format!("{}@{}", self.client, self.client_realm),
51            "service": format!("{}@{}", self.service, self.service_realm),
52            "authtime": codec::rfc3339(self.authtime),
53            "starttime": codec::rfc3339(self.starttime),
54            "endtime": codec::rfc3339(self.endtime),
55            "lifetime_secs": self.endtime - self.starttime,
56            "expires_in_secs": self.endtime - now,
57            "renew_till": self.renew_till.map(codec::rfc3339),
58            "flags": codec::flag_names(self.flags, codec::ticket_flags::NAMES),
59            "session_etype": codec::etype_name(self.session_key.etype()),
60            "ticket_etype": codec::etype_name(ticket_etype),
61        })
62    }
63}
64
65/// What the client asks for, beyond who and which service.
66#[derive(Debug, Clone, Default)]
67pub struct Request {
68    pub lifetime_secs: i64,
69    pub renewable_lifetime_secs: Option<i64>,
70    pub forwardable: bool,
71    pub proxiable: bool,
72    /// Client addresses to put in the request. The model cannot set these; the suites use them
73    /// to make a reply too big for one datagram.
74    pub addresses: Vec<std::net::IpAddr>,
75}
76
77impl Request {
78    fn options(&self) -> u32 {
79        let mut o = 0;
80        if self.forwardable {
81            o |= codec::kdc_options::FORWARDABLE;
82        }
83        if self.proxiable {
84            o |= codec::kdc_options::PROXIABLE;
85        }
86        if self.renewable_lifetime_secs.is_some() {
87            o |= codec::kdc_options::RENEWABLE;
88        }
89        o
90    }
91}
92
93/// A random nonce in 31 bits, which every implementation encodes the same way.
94pub fn nonce() -> u32 {
95    rand::random::<u32>() & 0x7fff_ffff
96}
97
98fn body(
99    client: Option<&Principal>,
100    realm: &str,
101    service: &Principal,
102    request: &Request,
103    nonce: u32,
104) -> Result<KdcReqBody> {
105    let now = now_secs();
106    Ok(KdcReqBody {
107        kdc_options: ExplicitContextTag0(flags(request.options())),
108        cname: Optional(match client {
109            Some(c) => Some(ExplicitContextTag1(c.to_asn1()?)),
110            None => None,
111        }),
112        realm: ExplicitContextTag2(kstring(realm)?),
113        sname: Optional(Some(ExplicitContextTag3(service.to_asn1()?))),
114        from: Optional(None),
115        till: ExplicitContextTag5(codec::ktime(now + request.lifetime_secs)),
116        rtime: Optional(
117            request
118                .renewable_lifetime_secs
119                .map(|r| ExplicitContextTag6(codec::ktime(now + r))),
120        ),
121        nonce: ExplicitContextTag7(int(nonce as i64)),
122        etype: ExplicitContextTag8(Asn1SequenceOf(
123            SUPPORTED_ETYPES.iter().map(|e| int(*e as i64)).collect(),
124        )),
125        addresses: Optional(if request.addresses.is_empty() {
126            None
127        } else {
128            Some(ExplicitContextTag9(Asn1SequenceOf(
129                request
130                    .addresses
131                    .iter()
132                    .map(|a| {
133                        let (kind, bytes) = match a {
134                            std::net::IpAddr::V4(v4) => (2, v4.octets().to_vec()),
135                            std::net::IpAddr::V6(v6) => (24, v6.octets().to_vec()),
136                        };
137                        HostAddress {
138                            addr_type: ExplicitContextTag0(int(kind)),
139                            address: ExplicitContextTag1(OctetStringAsn1(bytes)),
140                        }
141                    })
142                    .collect(),
143            )))
144        }),
145        enc_authorization_data: Optional(None),
146        additional_tickets: Optional(None),
147    })
148}
149
150/// PA-ENC-TIMESTAMP: the current time encrypted in the client's key (RFC 4120 §5.2.7.2).
151pub fn pa_enc_timestamp(key: &Key) -> Result<PaData> {
152    pa_enc_timestamp_at(key, now_secs())
153}
154
155/// PA-ENC-TIMESTAMP for an arbitrary time, for the suites that test the clock-skew bound.
156pub fn pa_enc_timestamp_at(key: &Key, secs: i64) -> Result<PaData> {
157    let ts = PaEncTsEnc {
158        patimestamp: ExplicitContextTag0(codec::ktime(secs)),
159        pausec: Optional(Some(ExplicitContextTag1(int(
160            (rand::random::<u32>() % 1_000_000) as i64,
161        )))),
162    };
163    let enc = key.encrypted_data(codec::KU_PA_ENC_TIMESTAMP, &encode(&ts)?, None)?;
164    Ok(codec::padata(PA_ENC_TIMESTAMP, encode(&enc)?))
165}
166
167/// An AS-REQ for `client` to `service`, with `padata` (empty for the first, unauthenticated
168/// attempt).
169pub fn as_req(
170    client: &Principal,
171    realm: &str,
172    service: &Principal,
173    request: &Request,
174    nonce: u32,
175    padata: Vec<PaData>,
176) -> Result<Vec<u8>> {
177    let req = AsReq::from(KdcReq {
178        pvno: ExplicitContextTag1(int(5)),
179        msg_type: ExplicitContextTag2(int(10)),
180        padata: Optional(if padata.is_empty() {
181            None
182        } else {
183            Some(ExplicitContextTag3(Asn1SequenceOf(padata)))
184        }),
185        req_body: ExplicitContextTag4(body(Some(client), realm, service, request, nonce)?),
186    });
187    encode(&req)
188}
189
190/// A TGS-REQ for `service`, authenticated with `tgt`, carrying `subkey` for the reply.
191pub fn tgs_req(
192    tgt: &Credential,
193    service: &Principal,
194    request: &Request,
195    nonce: u32,
196    subkey: &Key,
197) -> Result<Vec<u8>> {
198    let req_body = body(None, &tgt.client_realm, service, request, nonce)?;
199    let body_der = encode(&req_body)?;
200    let session = &tgt.session_key;
201    let checksum = Checksum {
202        cksumtype: ExplicitContextTag0(int(session.checksum_type() as i64)),
203        checksum: ExplicitContextTag1(OctetStringAsn1(
204            session.checksum(codec::KU_TGS_REQ_AUTH_CKSUM, &body_der)?,
205        )),
206    };
207    let authenticator = Authenticator::from(AuthenticatorInner {
208        authenticator_vno: ExplicitContextTag0(int(5)),
209        crealm: ExplicitContextTag1(kstring(&tgt.client_realm)?),
210        cname: ExplicitContextTag2(tgt.client.to_asn1()?),
211        cksum: Optional(Some(ExplicitContextTag3(checksum))),
212        cusec: ExplicitContextTag4(int((rand::random::<u32>() % 1_000_000) as i64)),
213        ctime: ExplicitContextTag5(codec::ktime(now_secs())),
214        subkey: Optional(Some(ExplicitContextTag6(subkey.to_asn1()))),
215        seq_number: Optional(None),
216        authorization_data: Optional(None),
217    });
218    let ap_req = ApReq::from(ApReqInner {
219        pvno: ExplicitContextTag0(int(5)),
220        msg_type: ExplicitContextTag1(int(14)),
221        ap_options: ExplicitContextTag2(flags(0)),
222        ticket: ExplicitContextTag3(tgt.ticket.clone()),
223        authenticator: ExplicitContextTag4(session.encrypted_data(
224            codec::KU_TGS_REQ_AUTHENTICATOR,
225            &encode(&authenticator)?,
226            None,
227        )?),
228    });
229    let req = TgsReq::from(KdcReq {
230        pvno: ExplicitContextTag1(int(5)),
231        msg_type: ExplicitContextTag2(int(12)),
232        padata: Optional(Some(ExplicitContextTag3(Asn1SequenceOf(vec![
233            codec::padata(PA_TGS_REQ, encode(&ap_req)?),
234        ])))),
235        req_body: ExplicitContextTag4(req_body),
236    });
237    let message = encode(&req)?;
238    // The checksum covers the body as the KDC will find it in the message; DER is
239    // deterministic, but this is the property the KDC checks, so check it here too.
240    if codec::req_body_bytes(&message) != Some(body_der.as_slice()) {
241        return Err(anyhow!(
242            "the encoded request body differs from the checksummed one"
243        ));
244    }
245    Ok(message)
246}
247
248/// A KDC reply, decoded but not yet trusted.
249pub enum Reply {
250    As(KdcRep),
251    Tgs(KdcRep),
252    Error(KrbErrorInner),
253}
254
255/// Decode a reply. Everything a KDC may send back is bounded by the same DER walk as requests.
256pub fn parse_reply(bytes: &[u8]) -> Result<Reply> {
257    match bytes.first() {
258        Some(&codec::TAG_AS_REP) => Ok(Reply::As(decode::<AsRep>(bytes)?.0)),
259        Some(&codec::TAG_TGS_REP) => Ok(Reply::Tgs(decode::<TgsRep>(bytes)?.0)),
260        Some(&codec::TAG_KRB_ERROR) => Ok(Reply::Error(decode::<KrbError>(bytes)?.0)),
261        Some(t) => Err(anyhow!(
262            "reply tag 0x{t:02x} is not an AS-REP, TGS-REP or KRB-ERROR"
263        )),
264        None => Err(anyhow!("empty reply")),
265    }
266}
267
268/// A KRB-ERROR as the model sees it.
269pub fn error_summary(e: &KrbErrorInner) -> serde_json::Value {
270    let code = e.error_code.0 as i32;
271    serde_json::json!({
272        "code": code,
273        "error": codec::error_name(code),
274        "text": e.e_text.0.as_ref().map(|t| crate::utils::sanitize::line_field(&kstring_text(&t.0))),
275    })
276}
277
278/// The `(etype, salt)` pairs of a PREAUTH_REQUIRED error's METHOD-DATA.
279pub fn etype_info_from_error(e: &KrbErrorInner) -> Vec<(i32, Option<String>)> {
280    let Some(data) = e.e_data.0.as_ref() else {
281        return Vec::new();
282    };
283    let Ok(method_data) = decode::<Asn1SequenceOf<PaData>>(&data.0 .0) else {
284        return Vec::new();
285    };
286    etype_info_from_padata(&method_data.0)
287}
288
289/// The `(etype, salt)` pairs of a PA-ETYPE-INFO2 in `padata`, if there is one.
290pub fn etype_info_from_padata(padata: &[PaData]) -> Vec<(i32, Option<String>)> {
291    let Some(pa) = padata
292        .iter()
293        .find(|p| codec::padata_type(p) == PA_ETYPE_INFO2 as i64)
294    else {
295        return Vec::new();
296    };
297    let Ok(info) = decode::<EtypeInfo2>(&pa.padata_data.0 .0) else {
298        return Vec::new();
299    };
300    info.0
301        .iter()
302        .filter_map(|e| {
303            let etype = int_i64(&e.etype.0).and_then(|v| i32::try_from(v).ok())?;
304            Some((etype, e.salt.0.as_ref().map(|s| kstring_text(&s.0))))
305        })
306        .collect()
307}
308
309/// What a verified reply must match.
310pub struct Expect<'a> {
311    pub client: &'a Principal,
312    pub client_realm: &'a str,
313    pub service: &'a Principal,
314    pub service_realm: &'a str,
315    pub nonce: u32,
316}
317
318/// Decrypt and verify a KDC-REP: the encrypted part must decrypt (its integrity check is the
319/// proof the KDC holds our key, or the TGT session key), echo our nonce and name the client and
320/// service we asked for. Only then is it a [`Credential`].
321pub fn verify_rep(rep: &KdcRep, key: &Key, usage: i32, expect: &Expect) -> Result<Credential> {
322    if int_i64(&rep.pvno.0) != Some(5) {
323        return Err(anyhow!("reply protocol version is not 5"));
324    }
325    let plain = key
326        .decrypt_data(usage, &rep.enc_part.0)
327        .map_err(|e| anyhow!("the reply's encrypted part did not verify: {e}"))?;
328    // RFC 4120 §5.4.2: accept either tag, as MIT and Heimdal both do.
329    let part: EncKdcRepPart = match decode::<EncAsRepPart>(&plain) {
330        Ok(p) => p.0,
331        Err(_) => {
332            decode::<EncTgsRepPart>(&plain)
333                .context("decrypted reply is neither EncASRepPart nor EncTGSRepPart")?
334                .0
335        }
336    };
337    let nonce_ok = int_i64(&part.nonce.0) == Some(expect.nonce as i64);
338    if !nonce_ok {
339        return Err(anyhow!("the reply does not echo our nonce"));
340    }
341    let cname = Principal::from_asn1(&rep.cname.0)?;
342    if !cname.same_name(expect.client) || kstring_text(&rep.crealm.0) != expect.client_realm {
343        return Err(anyhow!("the reply is for {cname}, not {}", expect.client));
344    }
345    let sname = Principal::from_asn1(&part.sname.0)?;
346    if !sname.same_name(expect.service) || kstring_text(&part.srealm.0) != expect.service_realm {
347        return Err(anyhow!(
348            "the reply is for service {sname}, not {}",
349            expect.service
350        ));
351    }
352    let tkt_sname = Principal::from_asn1(&rep.ticket.0 .0.sname.0)?;
353    if !tkt_sname.same_name(expect.service) {
354        return Err(anyhow!(
355            "the ticket is for {tkt_sname}, not {}",
356            expect.service
357        ));
358    }
359    let authtime = unix(&part.auth_time.0).context("authtime is not a date")?;
360    let endtime = unix(&part.end_time.0).context("endtime is not a date")?;
361    Ok(Credential {
362        client: expect.client.clone(),
363        client_realm: expect.client_realm.to_string(),
364        service: expect.service.clone(),
365        service_realm: expect.service_realm.to_string(),
366        ticket: rep.ticket.0.clone(),
367        session_key: Key::from_asn1(&part.key.0)?,
368        flags: flags_u32(&part.flags.0),
369        authtime,
370        starttime: part
371            .start_time
372            .0
373            .as_ref()
374            .and_then(|t| unix(&t.0))
375            .unwrap_or(authtime),
376        endtime,
377        renew_till: part.renew_till.0.as_ref().and_then(|t| unix(&t.0)),
378    })
379}
380
381/// Re-export for the test suites that inspect what NetGet's KDC put in a ticket's flags.
382pub fn ticket_flag_names(value: u32) -> Vec<String> {
383    codec::flag_names(value, codec::ticket_flags::NAMES)
384}