1use 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#[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 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#[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 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
93pub 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
150pub fn pa_enc_timestamp(key: &Key) -> Result<PaData> {
152 pa_enc_timestamp_at(key, now_secs())
153}
154
155pub 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
167pub 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
190pub 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 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
248pub enum Reply {
250 As(KdcRep),
251 Tgs(KdcRep),
252 Error(KrbErrorInner),
253}
254
255pub 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
268pub 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
278pub 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
289pub 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
309pub 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
318pub 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 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
381pub fn ticket_flag_names(value: u32) -> Vec<String> {
383 codec::flag_names(value, codec::ticket_flags::NAMES)
384}