sspi/kerberos/client/
generators.rs

1use std::env;
2use std::path::Path;
3use std::str::FromStr;
4
5use bitflags;
6use md5::{Digest, Md5};
7use picky_asn1::bit_string::BitString;
8use picky_asn1::date::GeneralizedTime;
9use picky_asn1::restricted_string::IA5String;
10use picky_asn1::wrapper::{
11    Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag11, ExplicitContextTag2,
12    ExplicitContextTag3, ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, ExplicitContextTag7,
13    ExplicitContextTag8, ExplicitContextTag9, GeneralizedTimeAsn1, IntegerAsn1, ObjectIdentifierAsn1, OctetStringAsn1,
14    Optional,
15};
16use picky_asn1_der::Asn1RawDer;
17use picky_asn1_x509::oids;
18use picky_krb::constants::gss_api::{
19    ACCEPT_COMPLETE, ACCEPT_INCOMPLETE, AP_REQ_TOKEN_ID, AUTHENTICATOR_CHECKSUM_TYPE, TGT_REQ_TOKEN_ID,
20};
21use picky_krb::constants::key_usages::{
22    AP_REP_ENC, AP_REQ_AUTHENTICATOR, KRB_PRIV_ENC_PART, TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR,
23};
24use picky_krb::constants::types::{
25    AD_AUTH_DATA_AP_OPTION_TYPE, AP_REP_MSG_TYPE, AP_REQ_MSG_TYPE, AS_REQ_MSG_TYPE, KERB_AP_OPTIONS_CBT, KRB_PRIV,
26    NET_BIOS_ADDR_TYPE, NT_ENTERPRISE, NT_PRINCIPAL, NT_SRV_INST, PA_ENC_TIMESTAMP, PA_ENC_TIMESTAMP_KEY_USAGE,
27    PA_PAC_OPTIONS_TYPE, PA_PAC_REQUEST_TYPE, PA_TGS_REQ_TYPE, TGS_REQ_MSG_TYPE, TGT_REQ_MSG_TYPE,
28};
29use picky_krb::crypto::CipherSuite;
30use picky_krb::data_types::{
31    ApOptions, Authenticator, AuthenticatorInner, AuthorizationData, AuthorizationDataInner, Checksum, EncApRepPart,
32    EncApRepPartInner, EncKrbPrivPart, EncKrbPrivPartInner, EncryptedData, EncryptionKey, HostAddress,
33    KerbPaPacRequest, KerberosFlags, KerberosStringAsn1, KerberosTime, PaData, PaEncTsEnc, PaPacOptions, PrincipalName,
34    Realm, Ticket,
35};
36use picky_krb::gss_api::{
37    ApplicationTag0, GssApiNegInit, KrbMessage, MechType, MechTypeList, NegTokenInit, NegTokenTarg, NegTokenTarg1,
38};
39use picky_krb::messages::{
40    ApMessage, ApRep, ApRepInner, ApReq, ApReqInner, AsReq, KdcRep, KdcReq, KdcReqBody, KrbPriv, KrbPrivInner,
41    KrbPrivMessage, TgsReq, TgtReq,
42};
43use rand::prelude::StdRng;
44use rand::{RngCore, SeedableRng};
45use time::{Duration, OffsetDateTime};
46
47use crate::channel_bindings::ChannelBindings;
48use crate::crypto::compute_md5_channel_bindings_hash;
49use crate::kerberos::flags::{ApOptions as ApOptionsFlags, KdcOptions};
50use crate::kerberos::{EncryptionParams, DEFAULT_ENCRYPTION_TYPE, KERBEROS_VERSION};
51use crate::krb::Krb5Conf;
52use crate::utils::parse_target_name;
53use crate::{ClientRequestFlags, Error, ErrorKind, Result};
54
55const TGT_TICKET_LIFETIME_DAYS: i64 = 3;
56const NONCE_LEN: usize = 4;
57/// [Microseconds](https://www.rfc-editor.org/rfc/rfc4120#section-5.2.4).
58/// The maximum microseconds value.
59///
60/// ```not_rust
61/// Microseconds    ::= INTEGER (0..999999)
62/// ```
63pub const MAX_MICROSECONDS: u32 = 999_999;
64const MD5_CHECKSUM_TYPE: [u8; 1] = [0x07];
65
66// Renewable, Canonicalize, and Renewable-ok are on by default
67// https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1
68pub const DEFAULT_AS_REQ_OPTIONS: [u8; 4] = [0x00, 0x81, 0x00, 0x10];
69
70// Renewable, Canonicalize.
71// https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1
72const DEFAULT_TGS_REQ_OPTIONS: [u8; 4] = [0x00, 0x81, 0x00, 0x00];
73
74const DEFAULT_PA_PAC_OPTIONS: [u8; 4] = [0x40, 0x00, 0x00, 0x00];
75
76/// [Authenticator Checksum](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1)
77///
78/// **Important**: the last 4 bytes are [Checksum Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1).
79/// This value should be set separately based on provided [CLientRequestFlags] or [GssFlags].
80pub const AUTHENTICATOR_DEFAULT_CHECKSUM: [u8; 24] = [
81    0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
82    0x00, 0x00, 0x00, 0x00, 0x00,
83];
84
85/// [MS-KILE] 3.3.5.6.1 Client Principal Lookup
86/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/6435d3fb-8cf6-4df5-a156-1277690ed59c
87pub fn get_client_principal_name_type(username: &str, _domain: &str) -> u8 {
88    if username.contains('@') {
89        NT_ENTERPRISE
90    } else {
91        NT_PRINCIPAL
92    }
93}
94
95pub fn get_client_principal_realm(username: &str, domain: &str) -> String {
96    // https://web.mit.edu/kerberos/krb5-current/doc/user/user_config/kerberos.html#environment-variables
97
98    let krb5_config = env::var("KRB5_CONFIG").unwrap_or_else(|_| "/etc/krb5.conf:/usr/local/etc/krb5.conf".to_string());
99    let krb5_conf_paths = krb5_config.split(':').map(Path::new).collect::<Vec<&Path>>();
100
101    get_client_principal_realm_impl(&krb5_conf_paths, username, domain)
102}
103
104fn get_client_principal_realm_impl(krb5_conf_paths: &[&Path], username: &str, domain: &str) -> String {
105    let domain = if domain.is_empty() {
106        if let Some((_left, right)) = username.split_once('@') {
107            right.to_string()
108        } else {
109            String::new()
110        }
111    } else {
112        domain.to_string()
113    };
114
115    for krb5_conf_path in krb5_conf_paths {
116        if !krb5_conf_path.exists() {
117            continue;
118        }
119
120        if let Some(krb5_conf) = Krb5Conf::new_from_file(krb5_conf_path) {
121            if let Some(mappings) = krb5_conf.get_values_in_section(&["domain_realm"]) {
122                for (mapping_domain, realm) in mappings {
123                    if matches_domain(&domain, mapping_domain) {
124                        return realm.to_owned();
125                    }
126                }
127            }
128        }
129    }
130
131    domain.to_uppercase()
132}
133
134fn matches_domain(domain: &str, mapping_domain: &str) -> bool {
135    if mapping_domain.starts_with('.') {
136        domain
137            .split_once('.')
138            .map(|(_, remaining)| remaining.eq_ignore_ascii_case(&mapping_domain[1..]))
139            .unwrap_or(false)
140    } else {
141        domain.eq_ignore_ascii_case(mapping_domain)
142    }
143}
144
145/// Parameters for generating pa-datas for [AsReq] message.
146#[derive(Debug)]
147pub struct GenerateAsPaDataOptions<'a> {
148    pub password: &'a str,
149    /// Salt for deriving the encryption key.
150    ///
151    /// The salt value should be extracted from the [KrbError] message.
152    pub salt: Vec<u8>,
153    pub enc_params: EncryptionParams,
154    /// Flag that indicates whether to generate pa-datas.
155    pub with_pre_auth: bool,
156}
157
158#[instrument(level = "trace", ret, skip_all, fields(options.salt, options.enc_params, options.with_pre_auth))]
159pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions<'_>) -> Result<Vec<PaData>> {
160    let GenerateAsPaDataOptions {
161        password,
162        salt,
163        enc_params,
164        with_pre_auth,
165    } = options;
166
167    let mut pa_datas = if *with_pre_auth {
168        let current_date = OffsetDateTime::now_utc();
169        let microseconds = current_date.microsecond().min(MAX_MICROSECONDS);
170
171        let timestamp = PaEncTsEnc {
172            patimestamp: ExplicitContextTag0::from(KerberosTime::from(GeneralizedTime::from(current_date))),
173            pausec: Optional::from(Some(ExplicitContextTag1::from(IntegerAsn1::from(
174                microseconds.to_be_bytes().to_vec(),
175            )))),
176        };
177        let timestamp_bytes = picky_asn1_der::to_vec(&timestamp)?;
178
179        let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
180        let cipher = encryption_type.cipher();
181
182        let key = cipher.generate_key_from_password(password.as_bytes(), salt)?;
183        trace!(?key, ?encryption_type, "AS timestamp encryption params",);
184
185        let encrypted_timestamp = cipher.encrypt(&key, PA_ENC_TIMESTAMP_KEY_USAGE, &timestamp_bytes)?;
186
187        trace!(
188            ?current_date,
189            ?microseconds,
190            ?timestamp_bytes,
191            ?encrypted_timestamp,
192            "Encrypted timestamp params",
193        );
194
195        vec![PaData {
196            padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ENC_TIMESTAMP.to_vec())),
197            padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&EncryptedData {
198                etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
199                kvno: Optional::from(None),
200                cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_timestamp)),
201            })?)),
202        }]
203    } else {
204        Vec::new()
205    };
206
207    pa_datas.push(PaData {
208        padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_PAC_REQUEST_TYPE.to_vec())),
209        padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&KerbPaPacRequest {
210            include_pac: ExplicitContextTag0::from(true),
211        })?)),
212    });
213
214    Ok(pa_datas)
215}
216
217/// Parameters for generating [AsReq].
218#[derive(Debug)]
219pub struct GenerateAsReqOptions<'a> {
220    pub realm: &'a str,
221    pub username: &'a str,
222    pub cname_type: u8,
223    pub snames: &'a [&'a str],
224    pub nonce: &'a [u8],
225    pub hostname: &'a str,
226    pub context_requirements: ClientRequestFlags,
227}
228
229#[instrument(level = "trace", ret)]
230pub fn generate_as_req_kdc_body(options: &GenerateAsReqOptions<'_>) -> Result<KdcReqBody> {
231    let GenerateAsReqOptions {
232        realm,
233        username,
234        cname_type,
235        snames,
236        nonce,
237        hostname: address,
238        context_requirements,
239    } = options;
240
241    let expiration_date = OffsetDateTime::now_utc()
242        .checked_add(Duration::days(TGT_TICKET_LIFETIME_DAYS))
243        .unwrap();
244
245    let host_address = HostAddress {
246        addr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NET_BIOS_ADDR_TYPE])),
247        address: ExplicitContextTag1::from(OctetStringAsn1::from(address.as_bytes().to_vec())),
248    };
249
250    let address = Some(ExplicitContextTag9::from(Asn1SequenceOf::from(vec![host_address])));
251
252    let mut service_names = Vec::with_capacity(snames.len());
253    for sname in *snames {
254        service_names.push(KerberosStringAsn1::from(IA5String::from_string((*sname).to_owned())?));
255    }
256
257    let mut as_req_options = KdcOptions::from_bits(u32::from_be_bytes(DEFAULT_AS_REQ_OPTIONS)).unwrap();
258    if context_requirements.contains(ClientRequestFlags::DELEGATE) {
259        as_req_options |= KdcOptions::FORWARDABLE;
260    }
261
262    Ok(KdcReqBody {
263        kdc_options: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes(
264            as_req_options.bits().to_be_bytes().to_vec(),
265        ))),
266        cname: Optional::from(Some(ExplicitContextTag1::from(PrincipalName {
267            name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![*cname_type])),
268            name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![KerberosStringAsn1::from(
269                IA5String::from_string((*username).into())?,
270            )])),
271        }))),
272        realm: ExplicitContextTag2::from(Realm::from(IA5String::from_string((*realm).into())?)),
273        sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName {
274            name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
275            name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(service_names)),
276        }))),
277        from: Optional::from(None),
278        till: ExplicitContextTag5::from(GeneralizedTimeAsn1::from(GeneralizedTime::from(expiration_date))),
279        rtime: Optional::from(Some(ExplicitContextTag6::from(GeneralizedTimeAsn1::from(
280            GeneralizedTime::from(expiration_date),
281        )))),
282        nonce: ExplicitContextTag7::from(IntegerAsn1::from(nonce.to_vec())),
283        etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![
284            IntegerAsn1::from(vec![CipherSuite::Aes256CtsHmacSha196.into()]),
285            IntegerAsn1::from(vec![CipherSuite::Aes128CtsHmacSha196.into()]),
286        ])),
287        addresses: Optional::from(address),
288        enc_authorization_data: Optional::from(None),
289        additional_tickets: Optional::from(None),
290    })
291}
292
293#[instrument(level = "debug", ret, skip_all)]
294pub fn generate_as_req(pa_datas: Vec<PaData>, kdc_req_body: KdcReqBody) -> AsReq {
295    AsReq::from(KdcReq {
296        pvno: ExplicitContextTag1::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
297        msg_type: ExplicitContextTag2::from(IntegerAsn1::from(vec![AS_REQ_MSG_TYPE])),
298        padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(pa_datas)))),
299        req_body: ExplicitContextTag4::from(kdc_req_body),
300    })
301}
302
303/// Parameters for generating [TgsReq].
304#[derive(Debug)]
305pub struct GenerateTgsReqOptions<'a> {
306    pub realm: &'a str,
307    pub service_principal: &'a str,
308    pub session_key: &'a [u8],
309    /// [Ticket] extracted from the [AsRep] message.
310    pub ticket: Ticket,
311    /// [Authenticator] to be included in [TgsReq] pa-data.
312    pub authenticator: &'a mut Authenticator,
313    /// If the Kerberos U2U auth is negotiated, then this parameter must have one ticket: TGT ticket of the application service.
314    /// Otherwise, set it to `None`.
315    pub additional_tickets: Option<Vec<Ticket>>,
316    pub enc_params: &'a EncryptionParams,
317    pub context_requirements: ClientRequestFlags,
318}
319
320#[instrument(level = "debug", ret)]
321pub fn generate_tgs_req(options: GenerateTgsReqOptions<'_>) -> Result<TgsReq> {
322    let GenerateTgsReqOptions {
323        realm,
324        service_principal,
325        session_key,
326        ticket,
327        authenticator,
328        additional_tickets,
329        enc_params,
330        context_requirements,
331    } = options;
332
333    let (service_name, service_principal_name) = parse_target_name(service_principal)?;
334
335    let expiration_date = OffsetDateTime::now_utc()
336        .checked_add(Duration::days(TGT_TICKET_LIFETIME_DAYS))
337        .unwrap();
338
339    let mut tgs_req_options = KdcOptions::from_bits(u32::from_be_bytes(DEFAULT_TGS_REQ_OPTIONS)).unwrap();
340    if context_requirements.contains(ClientRequestFlags::DELEGATE) {
341        tgs_req_options |= KdcOptions::FORWARDABLE;
342    }
343    if context_requirements.contains(ClientRequestFlags::USE_SESSION_KEY) {
344        tgs_req_options |= KdcOptions::ENC_TKT_IN_SKEY;
345    }
346
347    let mut rng = StdRng::try_from_os_rng()?;
348    let mut nonce = [0; NONCE_LEN];
349    rng.fill_bytes(&mut nonce);
350
351    let req_body = KdcReqBody {
352        kdc_options: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes(
353            tgs_req_options.bits().to_be_bytes().to_vec(),
354        ))),
355        cname: Optional::from(None),
356        realm: ExplicitContextTag2::from(Realm::from(IA5String::from_str(realm)?)),
357        sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName {
358            name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
359            name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![
360                KerberosStringAsn1::from(IA5String::from_string(service_name.into())?),
361                KerberosStringAsn1::from(IA5String::from_string(service_principal_name.into())?),
362            ])),
363        }))),
364        from: Optional::from(None),
365        till: ExplicitContextTag5::from(GeneralizedTimeAsn1::from(GeneralizedTime::from(expiration_date))),
366        rtime: Optional::from(None),
367        nonce: ExplicitContextTag7::from(IntegerAsn1::from(nonce.to_vec())),
368        etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![
369            IntegerAsn1::from(vec![CipherSuite::Aes256CtsHmacSha196.into()]),
370            IntegerAsn1::from(vec![CipherSuite::Aes128CtsHmacSha196.into()]),
371        ])),
372        addresses: Optional::from(None),
373        enc_authorization_data: Optional::from(None),
374        additional_tickets: Optional::from(
375            additional_tickets.map(|tickets| ExplicitContextTag11::from(Asn1SequenceOf::from(tickets))),
376        ),
377    };
378
379    let mut md5 = Md5::new();
380    md5.update(&picky_asn1_der::to_vec(&req_body)?);
381    let checksum = md5.finalize();
382
383    authenticator.0.cksum = Optional::from(Some(ExplicitContextTag3::from(Checksum {
384        cksumtype: ExplicitContextTag0::from(IntegerAsn1::from(MD5_CHECKSUM_TYPE.to_vec())),
385        checksum: ExplicitContextTag1::from(OctetStringAsn1::from(checksum.to_vec())),
386    })));
387
388    let pa_tgs_req =
389        PaData {
390            padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_TGS_REQ_TYPE.to_vec())),
391            padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(
392                &generate_tgs_ap_req(ticket, session_key, authenticator, enc_params)?,
393            )?)),
394        };
395
396    let pa_pac_options = PaData {
397        padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_PAC_OPTIONS_TYPE.to_vec())),
398        padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&PaPacOptions {
399            flags: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes(
400                DEFAULT_PA_PAC_OPTIONS.to_vec(),
401            ))),
402        })?)),
403    };
404
405    Ok(TgsReq::from(KdcReq {
406        pvno: ExplicitContextTag1::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
407        msg_type: ExplicitContextTag2::from(IntegerAsn1::from(vec![TGS_REQ_MSG_TYPE])),
408        padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(vec![
409            pa_tgs_req,
410            pa_pac_options,
411        ])))),
412        req_body: ExplicitContextTag4::from(req_body),
413    }))
414}
415
416#[derive(Debug)]
417pub struct ChecksumOptions {
418    pub checksum_type: Vec<u8>,
419    pub checksum_value: ChecksumValues,
420}
421
422#[derive(Debug)]
423pub struct ChecksumValues {
424    inner: Vec<u8>, // use named fields for future extensibility, this is a temporary solution
425}
426
427impl Default for ChecksumValues {
428    fn default() -> Self {
429        Self {
430            inner: AUTHENTICATOR_DEFAULT_CHECKSUM.to_vec(),
431        }
432    }
433}
434
435impl From<ChecksumValues> for Vec<u8> {
436    fn from(val: ChecksumValues) -> Self {
437        val.inner
438    }
439}
440
441impl From<[u8; 24]> for ChecksumValues {
442    fn from(bytes: [u8; 24]) -> Self {
443        ChecksumValues {
444            inner: Vec::from(bytes),
445        }
446    }
447}
448
449impl ChecksumValues {
450    pub(crate) fn set_flags(&mut self, flags: GssFlags) {
451        let flag_bits = flags.bits();
452        let flag_bytes = flag_bits.to_le_bytes();
453        self.inner[20..24].copy_from_slice(&flag_bytes);
454    }
455
456    pub(crate) fn into_inner(self) -> Vec<u8> {
457        self.inner
458    }
459}
460
461bitflags::bitflags! {
462    /// The checksum "Flags" field is used to convey service options or extension negotiation information.
463    /// More info:
464    /// * https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1
465    #[derive(Debug,Clone,Copy)]
466    pub(crate) struct GssFlags: u32 {
467        // [Checksum Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1).
468        const GSS_C_DELEG_FLAG      = 1;
469        const GSS_C_MUTUAL_FLAG     = 2;
470        const GSS_C_REPLAY_FLAG     = 4;
471        const GSS_C_SEQUENCE_FLAG   = 8;
472        const GSS_C_CONF_FLAG       = 16;
473        const GSS_C_INTEG_FLAG      = 32;
474
475        const GSS_C_ANON_FLAG       = 64;
476        const GSS_C_PROT_READY_FLAG = 128;
477        const GSS_C_TRANS_FLAG      = 256;
478        const GSS_C_DELEG_POLICY_FLAG = 32768;
479
480        // Additional GSS flags from MS-KILE specification:
481        // * [3.2.5.2 Authenticator Checksum Flags](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/387806fc-ed78-445e-afd8-c5639fe4a90a)
482
483        // [Mechanism Specific Changes](https://www.rfc-editor.org/rfc/rfc4757.html#section-7.1):
484        // Setting this flag indicates that the client wants to be informed of extended error information. In
485        // particular, Windows 2000 status codes may be returned in the data field of a Kerberos error message.
486        // This allows the client to understand a server failure more precisely.
487        const GSS_C_EXTENDED_ERROR_FLAG = 0x4000;
488        // This flag allows the client to indicate to the server that it should only allow the server application to identify
489        // the client by name and ID, but not to impersonate the client.
490        const GSS_C_IDENTIFY_FLAG = 0x2000;
491        // This flag was added for use with Microsoft's implementation of Distributed Computing Environment Remote Procedure
492        // Call (DCE RPC), which initially expected three legs of authentication.
493        const GSS_C_DCE_STYLE = 0x1000;
494    }
495}
496
497impl From<ClientRequestFlags> for GssFlags {
498    /*
499       the semantics of some of the flags of SSPI are I believe one to one mapped to the GSS flags
500    */
501    fn from(value: ClientRequestFlags) -> Self {
502        let mut flags = GssFlags::empty();
503
504        if value.contains(ClientRequestFlags::DELEGATE) {
505            flags |= GssFlags::GSS_C_DELEG_FLAG;
506        }
507
508        if value.contains(ClientRequestFlags::MUTUAL_AUTH) {
509            flags |= GssFlags::GSS_C_MUTUAL_FLAG;
510        }
511
512        if value.contains(ClientRequestFlags::REPLAY_DETECT) {
513            flags |= GssFlags::GSS_C_REPLAY_FLAG;
514        }
515
516        if value.contains(ClientRequestFlags::SEQUENCE_DETECT) {
517            flags |= GssFlags::GSS_C_SEQUENCE_FLAG;
518        }
519
520        if value.contains(ClientRequestFlags::CONFIDENTIALITY) {
521            flags |= GssFlags::GSS_C_CONF_FLAG;
522        }
523
524        if value.contains(ClientRequestFlags::INTEGRITY) {
525            flags |= GssFlags::GSS_C_INTEG_FLAG;
526        }
527
528        if value.contains(ClientRequestFlags::NO_INTEGRITY) {
529            flags &= !GssFlags::GSS_C_INTEG_FLAG;
530        }
531
532        if value.contains(ClientRequestFlags::USE_DCE_STYLE) {
533            flags |= GssFlags::GSS_C_DCE_STYLE;
534        }
535
536        flags
537    }
538}
539
540#[derive(Debug)]
541pub struct AuthenticatorChecksumExtension {
542    pub extension_type: u32,
543    pub extension_value: Vec<u8>,
544}
545
546/// Encryption key.
547#[derive(Debug)]
548pub struct EncKey {
549    /// Encryption type.
550    pub key_type: CipherSuite,
551    /// Encryption key value.
552    pub key_value: Vec<u8>,
553}
554
555/// Input parameters for generating ApReq Authenticator.
556#[derive(Debug)]
557pub struct GenerateAuthenticatorOptions<'a> {
558    /// [KdcRep] from previous interaction with KDC.
559    pub kdc_rep: &'a KdcRep,
560    /// Sequence number.
561    pub seq_num: Option<u32>,
562    /// Sub-session encryption key.
563    pub sub_key: Option<EncKey>,
564    /// Authenticator checksum options.
565    pub checksum: Option<ChecksumOptions>,
566    /// Channel bindings.
567    pub channel_bindings: Option<&'a ChannelBindings>,
568    /// Possible authenticator extensions.
569    pub extensions: Vec<AuthenticatorChecksumExtension>,
570}
571
572/// Generated ApReq Authenticator.
573#[instrument(level = "trace", ret)]
574pub fn generate_authenticator(options: GenerateAuthenticatorOptions<'_>) -> Result<Authenticator> {
575    let GenerateAuthenticatorOptions {
576        kdc_rep,
577        seq_num,
578        sub_key,
579        checksum,
580        channel_bindings,
581        ..
582    } = options;
583
584    let current_date = OffsetDateTime::now_utc();
585    let mut microseconds = current_date.microsecond();
586    if microseconds > MAX_MICROSECONDS {
587        microseconds = MAX_MICROSECONDS;
588    }
589
590    let authorization_data = Optional::from(channel_bindings.as_ref().map(|_| {
591        ExplicitContextTag8::from(AuthorizationData::from(vec![AuthorizationDataInner {
592            ad_type: ExplicitContextTag0::from(IntegerAsn1::from(AD_AUTH_DATA_AP_OPTION_TYPE.to_vec())),
593            ad_data: ExplicitContextTag1::from(OctetStringAsn1::from(KERB_AP_OPTIONS_CBT.to_vec())),
594        }]))
595    }));
596
597    let cksum = if let Some(ChecksumOptions {
598        checksum_type,
599        checksum_value,
600    }) = checksum
601    {
602        let mut checksum_value = checksum_value.into_inner();
603        if checksum_type == AUTHENTICATOR_CHECKSUM_TYPE && channel_bindings.is_some() {
604            if checksum_value.len() < 20 {
605                return Err(Error::new(
606                    ErrorKind::InvalidParameter,
607                    format!(
608                        "Invalid authenticator checksum length: expected >= 20 but got {}. ",
609                        checksum_value.len()
610                    ),
611                ));
612            }
613            // [Authenticator Checksum](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1)
614            // 4..19 - Channel binding information (19 inclusive).
615            checksum_value[4..20]
616                .copy_from_slice(&compute_md5_channel_bindings_hash(channel_bindings.as_ref().unwrap()));
617        }
618        Optional::from(Some(ExplicitContextTag3::from(Checksum {
619            cksumtype: ExplicitContextTag0::from(IntegerAsn1::from(checksum_type)),
620            checksum: ExplicitContextTag1::from(OctetStringAsn1::from(checksum_value)),
621        })))
622    } else {
623        Optional::from(None)
624    };
625
626    Ok(Authenticator::from(AuthenticatorInner {
627        authenticator_vno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
628        crealm: ExplicitContextTag1::from(kdc_rep.crealm.0.clone()),
629        cname: ExplicitContextTag2::from(kdc_rep.cname.0.clone()),
630        cksum,
631        cusec: ExplicitContextTag4::from(IntegerAsn1::from(microseconds.to_be_bytes().to_vec())),
632        ctime: ExplicitContextTag5::from(KerberosTime::from(GeneralizedTime::from(current_date))),
633        subkey: Optional::from(sub_key.map(|EncKey { key_type, key_value }| {
634            ExplicitContextTag6::from(EncryptionKey {
635                key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![key_type.into()])),
636                key_value: ExplicitContextTag1::from(OctetStringAsn1::from(key_value)),
637            })
638        })),
639        seq_number: Optional::from(seq_num.map(|seq_num| {
640            ExplicitContextTag7::from(IntegerAsn1::from_bytes_be_unsigned(seq_num.to_be_bytes().to_vec()))
641        })),
642        authorization_data,
643    }))
644}
645
646#[instrument(level = "trace", skip_all, ret)]
647pub fn generate_ap_rep(session_key: &[u8], seq_number: Vec<u8>, enc_params: &EncryptionParams) -> Result<ApRep> {
648    let current_date = OffsetDateTime::now_utc();
649    let microseconds = current_date.microsecond().min(MAX_MICROSECONDS);
650
651    let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
652
653    let enc_ap_rep_part = EncApRepPart::from(EncApRepPartInner {
654        ctime: ExplicitContextTag0::from(KerberosTime::from(GeneralizedTime::from(current_date))),
655        cusec: ExplicitContextTag1::from(IntegerAsn1::from(microseconds.to_be_bytes().to_vec())),
656        subkey: Optional::from(None),
657        seq_number: Optional::from(Some(ExplicitContextTag3::from(IntegerAsn1::from(seq_number)))),
658    });
659
660    let cipher = encryption_type.cipher();
661
662    let encoded_enc_ap_rep_part = picky_asn1_der::to_vec(&enc_ap_rep_part)?;
663    let encrypted_enc_ap_rep_part = cipher.encrypt(session_key, AP_REP_ENC, &encoded_enc_ap_rep_part)?;
664
665    Ok(ApRep::from(ApRepInner {
666        pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
667        msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AP_REP_MSG_TYPE])),
668        enc_part: ExplicitContextTag2::from(EncryptedData {
669            etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
670            kvno: Optional::from(None),
671            cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_enc_ap_rep_part)),
672        }),
673    }))
674}
675
676pub fn generate_tgs_ap_req(
677    ticket: Ticket,
678    session_key: &[u8],
679    authenticator: &Authenticator,
680    enc_params: &EncryptionParams,
681) -> Result<ApReq> {
682    let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
683    let cipher = encryption_type.cipher();
684
685    let encoded_authenticator = picky_asn1_der::to_vec(&authenticator)?;
686    let encrypted_authenticator = cipher.encrypt(
687        session_key,
688        TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR,
689        &encoded_authenticator,
690    )?;
691
692    trace!(
693        ?session_key,
694        ?encryption_type,
695        "TGS AP_REQ authenticator encryption params",
696    );
697    trace!(
698        plain = ?encoded_authenticator,
699        encrypted = ?encrypted_authenticator,
700        "TGS AP_REQ authenticator",
701    );
702
703    Ok(ApReq::from(ApReqInner {
704        pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
705        msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AP_REQ_MSG_TYPE])),
706        ap_options: ExplicitContextTag2::from(ApOptions::from(BitString::with_bytes(vec![
707            // do not need any options when ap_req uses in tgs_req pa_data
708            0x00, 0x00, 0x00, 0x00,
709        ]))),
710        ticket: ExplicitContextTag3::from(ticket),
711        authenticator: ExplicitContextTag4::from(EncryptedData {
712            etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
713            kvno: Optional::from(None),
714            cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_authenticator)),
715        }),
716    }))
717}
718
719#[instrument(level = "trace", ret)]
720pub fn generate_ap_req(
721    ticket: Ticket,
722    session_key: &[u8],
723    authenticator: &Authenticator,
724    enc_params: &EncryptionParams,
725    options: ApOptionsFlags,
726) -> Result<ApReq> {
727    let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
728    let cipher = encryption_type.cipher();
729
730    let encoded_authenticator = picky_asn1_der::to_vec(&authenticator)?;
731    let encrypted_authenticator = cipher.encrypt(session_key, AP_REQ_AUTHENTICATOR, &encoded_authenticator)?;
732
733    trace!(
734        plain = ?encoded_authenticator,
735        encrypted = ?encrypted_authenticator,
736        "AP_REQ authenticator",
737    );
738
739    Ok(ApReq::from(ApReqInner {
740        pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
741        msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AP_REQ_MSG_TYPE])),
742        ap_options: ExplicitContextTag2::from(ApOptions::from(BitString::with_bytes(
743            options.bits().to_be_bytes().to_vec(),
744        ))),
745        ticket: ExplicitContextTag3::from(ticket),
746        authenticator: ExplicitContextTag4::from(EncryptedData {
747            etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
748            kvno: Optional::from(None),
749            cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_authenticator)),
750        }),
751    }))
752}
753
754/// Returns supported authentication types.
755pub fn get_mech_list() -> MechTypeList {
756    MechTypeList::from(vec![MechType::from(oids::ms_krb5()), MechType::from(oids::krb5())])
757}
758
759/// Generates the initial SPNEGO token.
760///
761/// The `sname` parameter is optional. If it is present, then the Kerberos U2U is in use, and `TgtReq` will be generated
762/// for the input `sname` and placed in the `mech_token` field.
763pub fn generate_neg_token_init(sname: Option<&[&str]>) -> Result<ApplicationTag0<GssApiNegInit>> {
764    let mech_token = if let Some(sname) = sname {
765        let sname = sname
766            .iter()
767            .map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string(sname.to_string())?)))
768            .collect::<Result<Vec<_>>>()?;
769
770        let krb5_neg_token_init = ApplicationTag0(KrbMessage {
771            krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()),
772            krb5_token_id: TGT_REQ_TOKEN_ID,
773            krb_msg: TgtReq {
774                pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
775                msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REQ_MSG_TYPE])),
776                server_name: ExplicitContextTag2::from(PrincipalName {
777                    name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
778                    name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(sname)),
779                }),
780            },
781        });
782
783        Some(ExplicitContextTag2::from(OctetStringAsn1::from(
784            picky_asn1_der::to_vec(&krb5_neg_token_init)?,
785        )))
786    } else {
787        None
788    };
789
790    Ok(ApplicationTag0(GssApiNegInit {
791        oid: ObjectIdentifierAsn1::from(oids::spnego()),
792        neg_token_init: ExplicitContextTag0::from(NegTokenInit {
793            mech_types: Optional::from(Some(ExplicitContextTag0::from(get_mech_list()))),
794            req_flags: Optional::from(None),
795            mech_token: Optional::from(mech_token),
796            mech_list_mic: Optional::from(None),
797        }),
798    }))
799}
800
801pub fn generate_neg_ap_req(ap_req: ApReq, mech_id: oid::ObjectIdentifier) -> Result<ExplicitContextTag1<NegTokenTarg>> {
802    let krb_blob = ApplicationTag0(KrbMessage {
803        krb5_oid: ObjectIdentifierAsn1::from(mech_id),
804        krb5_token_id: AP_REQ_TOKEN_ID,
805        krb_msg: ap_req,
806    });
807
808    Ok(ExplicitContextTag1::from(NegTokenTarg {
809        neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_INCOMPLETE.to_vec())))),
810        supported_mech: Optional::from(None),
811        response_token: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(
812            picky_asn1_der::to_vec(&krb_blob)?,
813        )))),
814        mech_list_mic: Optional::from(None),
815    }))
816}
817
818pub fn generate_final_neg_token_targ(mech_list_mic: Option<Vec<u8>>) -> NegTokenTarg1 {
819    NegTokenTarg1::from(NegTokenTarg {
820        neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_COMPLETE.to_vec())))),
821        supported_mech: Optional::from(None),
822        response_token: Optional::from(None),
823        mech_list_mic: Optional::from(mech_list_mic.map(|v| ExplicitContextTag3::from(OctetStringAsn1::from(v)))),
824    })
825}
826
827#[instrument(level = "trace", ret)]
828pub fn generate_krb_priv_request(
829    ticket: Ticket,
830    session_key: &[u8],
831    new_password: &[u8],
832    authenticator: &Authenticator,
833    enc_params: &EncryptionParams,
834    seq_num: u32,
835    address: &str,
836) -> Result<KrbPrivMessage> {
837    let ap_req = generate_ap_req(ticket, session_key, authenticator, enc_params, ApOptionsFlags::empty())?;
838
839    let enc_part = EncKrbPrivPart::from(EncKrbPrivPartInner {
840        user_data: ExplicitContextTag0::from(OctetStringAsn1::from(new_password.to_vec())),
841        timestamp: Optional::from(None),
842        usec: Optional::from(None),
843        seq_number: Optional::from(Some(ExplicitContextTag3::from(IntegerAsn1::from_bytes_be_unsigned(
844            seq_num.to_be_bytes().to_vec(),
845        )))),
846        s_address: ExplicitContextTag4::from(HostAddress {
847            addr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NET_BIOS_ADDR_TYPE])),
848            address: ExplicitContextTag1::from(OctetStringAsn1::from(address.as_bytes().to_vec())),
849        }),
850        r_address: Optional::from(None),
851    });
852
853    let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
854    let cipher = encryption_type.cipher();
855
856    let encryption_key = &authenticator.0.subkey.0.as_ref().unwrap().key_value.0;
857    let encoded_krb_priv = picky_asn1_der::to_vec(&enc_part)?;
858
859    let enc_part = cipher.encrypt(encryption_key, KRB_PRIV_ENC_PART, &encoded_krb_priv)?;
860
861    trace!(?encryption_key, ?encryption_type, "KRB_PRIV encryption params",);
862    trace!(
863        plain = ?encoded_krb_priv,
864        encrypted = ?enc_part,
865        "KRB_PRIV encrypted part",
866    );
867
868    let krb_priv = KrbPriv::from(KrbPrivInner {
869        pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
870        msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![KRB_PRIV])),
871        enc_part: ExplicitContextTag3::from(EncryptedData {
872            etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
873            kvno: Optional::from(None),
874            cipher: ExplicitContextTag2::from(OctetStringAsn1::from(enc_part)),
875        }),
876    });
877
878    Ok(KrbPrivMessage {
879        ap_message: ApMessage::ApReq(ap_req),
880        krb_priv,
881    })
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887
888    const KRB5_CONFIG_FILE_PATH: &str = "test_assets/krb5.conf";
889
890    #[test]
891    fn test_set_flags() {
892        let mut checksum_values = ChecksumValues::default();
893        let flags = GssFlags::GSS_C_MUTUAL_FLAG | GssFlags::GSS_C_REPLAY_FLAG;
894        checksum_values.set_flags(flags);
895        let expected_bytes = flags.bits().to_le_bytes();
896        assert_eq!(checksum_values.inner[20..24], expected_bytes);
897    }
898
899    #[test]
900    fn test_default() {
901        // ensure backwards compatibility
902        let checksum_values = ChecksumValues::default();
903        assert_eq!(checksum_values.into_inner(), AUTHENTICATOR_DEFAULT_CHECKSUM);
904    }
905
906    #[test]
907    fn test_flag_for_sign_and_seal() {
908        let mut checksum_values = ChecksumValues::default();
909        let flags = GssFlags::GSS_C_MUTUAL_FLAG
910            | GssFlags::GSS_C_REPLAY_FLAG
911            | GssFlags::GSS_C_SEQUENCE_FLAG
912            | GssFlags::GSS_C_CONF_FLAG
913            | GssFlags::GSS_C_INTEG_FLAG;
914        checksum_values.set_flags(flags);
915        let expected_bytes = [0x3E, 0x00, 0x00, 0x00];
916        assert_eq!(checksum_values.inner[20..24], expected_bytes);
917    }
918
919    #[test]
920    fn test_get_client_principal_realm_from_domain() {
921        let username = "";
922        let domain = "TBT.com";
923
924        let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], username, domain);
925
926        assert_eq!(realm, "TBT.COM");
927    }
928
929    #[test]
930    fn test_get_client_principal_realm_from_username() {
931        let username = "user@tbt.com";
932        let domain = "";
933
934        let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], username, domain);
935
936        assert_eq!(realm, "TBT.COM");
937    }
938
939    #[test]
940    fn test_get_client_principal_realm_from_subdomain_and_domain() {
941        let username = "";
942        let domain = "s.tbt.com";
943
944        let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], username, domain);
945
946        assert_eq!(realm, "TBT.COM");
947    }
948}