|
| 1 | +use std::time::Duration; |
| 2 | + |
| 3 | +use argon2::password_hash::rand_core::{OsRng, RngCore as _}; |
| 4 | +use picky_asn1::restricted_string::IA5String; |
| 5 | +use picky_asn1::wrapper::{ |
| 6 | + Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, |
| 7 | + ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, IntegerAsn1, OctetStringAsn1, Optional, |
| 8 | +}; |
| 9 | +use picky_krb::constants::etypes::{AES128_CTS_HMAC_SHA1_96, AES256_CTS_HMAC_SHA1_96}; |
| 10 | +use picky_krb::constants::key_usages::AS_REP_ENC; |
| 11 | +use picky_krb::constants::types::{ |
| 12 | + AS_REP_MSG_TYPE, AS_REQ_MSG_TYPE, ENC_AS_REP_PART_TYPE, PA_ENC_TIMESTAMP, PA_ENC_TIMESTAMP_KEY_USAGE, |
| 13 | + PA_ETYPE_INFO2_TYPE, |
| 14 | +}; |
| 15 | +use picky_krb::crypto::CipherSuite; |
| 16 | +use picky_krb::data_types::{EncryptedData, EtypeInfo2Entry, KerberosStringAsn1, PaData, PaEncTsEnc}; |
| 17 | +use picky_krb::messages::{AsRep, AsReq, KdcRep, KdcReq, KdcReqBody}; |
| 18 | +use sspi::kerberos::TGT_SERVICE_NAME; |
| 19 | +use sspi::{KERBEROS_VERSION, Secret}; |
| 20 | +use time::OffsetDateTime; |
| 21 | + |
| 22 | +use crate::config::{DomainUser, KerberosServer}; |
| 23 | +use crate::error::KdcError; |
| 24 | +use crate::ticket::{MakeTicketParams, RepEncPartParams, make_rep_enc_part, make_ticket}; |
| 25 | +use crate::{find_user_credentials, validate_request_from_and_till, validate_request_sname}; |
| 26 | + |
| 27 | +/// Validates AS-REQ PA-DATAs. |
| 28 | +/// |
| 29 | +/// The current implementation accepts only [PA_ENC_TIMESTAMP] pa-data (i.e. password-based logon). |
| 30 | +/// [PA_PK_AS_REQ] pa-data (i.e. scard-based logon) is not supported. |
| 31 | +fn validate_pa_data_timestamp( |
| 32 | + domain_user: &DomainUser, |
| 33 | + max_time_skew: u64, |
| 34 | + pa_datas: &[PaData], |
| 35 | +) -> Result<Secret<Vec<u8>>, KdcError> { |
| 36 | + let pa_data = pa_datas |
| 37 | + .iter() |
| 38 | + .find_map(|pa_data| { |
| 39 | + if pa_data.padata_type.0.0 == PA_ENC_TIMESTAMP { |
| 40 | + Some(pa_data.padata_data.0.0.as_slice()) |
| 41 | + } else { |
| 42 | + None |
| 43 | + } |
| 44 | + }) |
| 45 | + .ok_or(KdcError::PreAuthRequired( |
| 46 | + "PA_ENC_TIMESTAMP is not present in AS_REQ padata", |
| 47 | + ))?; |
| 48 | + |
| 49 | + let encrypted_timestamp: EncryptedData = |
| 50 | + picky_asn1_der::from_bytes(pa_data).map_err(|_| KdcError::PreAuthFailed("unable to decode pa-data value"))?; |
| 51 | + |
| 52 | + let cipher = CipherSuite::try_from(encrypted_timestamp.etype.0.0.as_slice()) |
| 53 | + .map_err(|_| KdcError::PreAuthFailed("invalid etype in PA_ENC_TIMESTAMP"))? |
| 54 | + .cipher(); |
| 55 | + let key = Secret::new( |
| 56 | + cipher |
| 57 | + .generate_key_from_password(domain_user.password.as_bytes(), domain_user.salt.as_bytes()) |
| 58 | + .map_err(|_| KdcError::InternalError("failed to generate user's key"))?, |
| 59 | + ); |
| 60 | + |
| 61 | + let timestamp_data = cipher |
| 62 | + .decrypt( |
| 63 | + key.as_ref(), |
| 64 | + PA_ENC_TIMESTAMP_KEY_USAGE, |
| 65 | + &encrypted_timestamp.cipher.0.0, |
| 66 | + ) |
| 67 | + .map_err(|_| KdcError::Modified("PA_ENC_TIMESTAMP"))?; |
| 68 | + let timestamp: PaEncTsEnc = picky_asn1_der::from_bytes(×tamp_data) |
| 69 | + .map_err(|_| KdcError::PreAuthFailed("unable to decode PaEncTsEnc value"))?; |
| 70 | + |
| 71 | + let client_timestamp = OffsetDateTime::try_from(timestamp.patimestamp.0.0) |
| 72 | + .map_err(|_| KdcError::PreAuthFailed("unable to decode PaEncTsEnc timestamp value"))?; |
| 73 | + let current = OffsetDateTime::now_utc(); |
| 74 | + |
| 75 | + if client_timestamp > current || current - client_timestamp > Duration::from_secs(max_time_skew) { |
| 76 | + return Err(KdcError::ClockSkew("invalid pa-data: clock skew too great")); |
| 77 | + } |
| 78 | + |
| 79 | + Ok(key) |
| 80 | +} |
| 81 | + |
| 82 | +/// Performs AS exchange according to the RFC 4120. |
| 83 | +/// |
| 84 | +/// RFC: [The Authentication Service Exchange](https://www.rfc-editor.org/rfc/rfc4120#section-3.1). |
| 85 | +pub(super) fn handle_as_req(as_req: &AsReq, kdc_config: &KerberosServer) -> Result<AsRep, KdcError> { |
| 86 | + let KdcReq { |
| 87 | + pvno, |
| 88 | + msg_type, |
| 89 | + padata, |
| 90 | + req_body, |
| 91 | + } = &as_req.0; |
| 92 | + |
| 93 | + if pvno.0.0 != [KERBEROS_VERSION] { |
| 94 | + return Err(KdcError::BadKrbVersion { |
| 95 | + version: pvno.0.0.clone(), |
| 96 | + expected: KERBEROS_VERSION, |
| 97 | + }); |
| 98 | + } |
| 99 | + |
| 100 | + if msg_type.0.0 != [AS_REQ_MSG_TYPE] { |
| 101 | + return Err(KdcError::BadMsgType { |
| 102 | + msg_type: msg_type.0.0.clone(), |
| 103 | + expected: AS_REQ_MSG_TYPE, |
| 104 | + }); |
| 105 | + } |
| 106 | + |
| 107 | + let KdcReqBody { |
| 108 | + kdc_options, |
| 109 | + cname, |
| 110 | + realm: realm_asn1, |
| 111 | + sname, |
| 112 | + from, |
| 113 | + till, |
| 114 | + rtime: _, |
| 115 | + nonce, |
| 116 | + etype, |
| 117 | + addresses, |
| 118 | + enc_authorization_data: _, |
| 119 | + additional_tickets: _, |
| 120 | + } = &req_body.0; |
| 121 | + |
| 122 | + let sname = sname |
| 123 | + .0 |
| 124 | + .clone() |
| 125 | + .ok_or(KdcError::InvalidSname( |
| 126 | + "sname is not present in KDC request sname".to_owned(), |
| 127 | + ))? |
| 128 | + .0; |
| 129 | + // The AS_REQ service name must meet the following requirements: |
| 130 | + // * The first string in sname must be equal to TGT_SERVICE_NAME. |
| 131 | + // * The second string in sname must be equal to KDC realm. |
| 132 | + validate_request_sname(&sname, &[TGT_SERVICE_NAME, &kdc_config.realm])?; |
| 133 | + |
| 134 | + let realm = realm_asn1.0.0.as_utf8(); |
| 135 | + if !realm.eq_ignore_ascii_case(&kdc_config.realm) { |
| 136 | + return Err(KdcError::WrongRealm(realm.to_owned())); |
| 137 | + } |
| 138 | + |
| 139 | + let cname = &cname |
| 140 | + .0 |
| 141 | + .as_ref() |
| 142 | + .ok_or(KdcError::ClientPrincipalUnknown( |
| 143 | + "the incoming KDC request does not contain client principal name".to_owned(), |
| 144 | + ))? |
| 145 | + .0; |
| 146 | + let domain_user = find_user_credentials(cname, realm, kdc_config)?; |
| 147 | + |
| 148 | + let pa_datas = &padata |
| 149 | + .0 |
| 150 | + .as_ref() |
| 151 | + .ok_or(KdcError::PreAuthRequired("pa-data is missing in incoming AS_REQ"))? |
| 152 | + .0 |
| 153 | + .0; |
| 154 | + |
| 155 | + let user_key = validate_pa_data_timestamp(domain_user, kdc_config.max_time_skew, pa_datas)?; |
| 156 | + let as_req_nonce = nonce.0.0.clone(); |
| 157 | + let etype_raw = etype |
| 158 | + .0 |
| 159 | + .0 |
| 160 | + .iter() |
| 161 | + .find(|etype| { |
| 162 | + // We support only AES256_CTS_HMAC_SHA1_96 and AES128_CTS_HMAC_SHA1_96. According to the RFC (https://datatracker.ietf.org/doc/html/rfc4120#section-3.1.3): |
| 163 | + // > The KDC will not issue tickets with a weak session key encryption type. |
| 164 | + if let Some(etype) = etype.0.first().copied().map(usize::from) { |
| 165 | + etype == AES256_CTS_HMAC_SHA1_96 || etype == AES128_CTS_HMAC_SHA1_96 |
| 166 | + } else { |
| 167 | + false |
| 168 | + } |
| 169 | + }) |
| 170 | + .ok_or(KdcError::NoSuitableEtype)? |
| 171 | + .0 |
| 172 | + .as_slice(); |
| 173 | + let etype = CipherSuite::try_from(etype_raw).map_err(|_| KdcError::NoSuitableEtype)?; |
| 174 | + let cipher = etype.cipher(); |
| 175 | + let realm = realm_asn1.0.clone(); |
| 176 | + let (auth_time, end_time) = validate_request_from_and_till(from.0.as_deref(), &till.0, kdc_config.max_time_skew)?; |
| 177 | + |
| 178 | + let mut rng = OsRng; |
| 179 | + let mut session_key = vec![0; cipher.key_size()]; |
| 180 | + rng.fill_bytes(&mut session_key); |
| 181 | + |
| 182 | + let as_rep_enc_data = make_rep_enc_part::<ENC_AS_REP_PART_TYPE>( |
| 183 | + RepEncPartParams { |
| 184 | + etype: etype.clone(), |
| 185 | + session_key: session_key.clone(), |
| 186 | + nonce: as_req_nonce, |
| 187 | + kdc_options: kdc_options.0.clone(), |
| 188 | + auth_time, |
| 189 | + end_time, |
| 190 | + realm: realm.clone(), |
| 191 | + sname: sname.clone(), |
| 192 | + addresses: addresses.0.clone().map(|addresses| addresses.0), |
| 193 | + }, |
| 194 | + user_key.as_ref(), |
| 195 | + AS_REP_ENC, |
| 196 | + )?; |
| 197 | + |
| 198 | + Ok(AsRep::from(KdcRep { |
| 199 | + pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), |
| 200 | + msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AS_REP_MSG_TYPE])), |
| 201 | + padata: Optional::from(Some(ExplicitContextTag2::from(Asn1SequenceOf::from(vec![PaData { |
| 202 | + padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ETYPE_INFO2_TYPE.to_vec())), |
| 203 | + padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec( |
| 204 | + &Asn1SequenceOf::from(vec![EtypeInfo2Entry { |
| 205 | + etype: ExplicitContextTag0::from(IntegerAsn1::from(etype_raw.to_vec())), |
| 206 | + salt: Optional::from(Some(ExplicitContextTag1::from(KerberosStringAsn1::from( |
| 207 | + IA5String::from_string(domain_user.salt.clone()).expect("salt to be a valid KerberosString"), |
| 208 | + )))), |
| 209 | + s2kparams: Optional::from(None), |
| 210 | + }]), |
| 211 | + )?)), |
| 212 | + }])))), |
| 213 | + crealm: ExplicitContextTag3::from(realm.clone()), |
| 214 | + cname: ExplicitContextTag4::from(cname.clone()), |
| 215 | + ticket: ExplicitContextTag5::from(make_ticket(MakeTicketParams { |
| 216 | + realm, |
| 217 | + session_key, |
| 218 | + ticket_encryption_key: &kdc_config.krbtgt_key, |
| 219 | + kdc_options: kdc_options.0.clone(), |
| 220 | + sname, |
| 221 | + cname: cname.clone(), |
| 222 | + etype, |
| 223 | + auth_time, |
| 224 | + end_time, |
| 225 | + })?), |
| 226 | + enc_part: ExplicitContextTag6::from(EncryptedData { |
| 227 | + etype: ExplicitContextTag0::from(IntegerAsn1::from(etype_raw.to_vec())), |
| 228 | + kvno: Optional::from(None), |
| 229 | + cipher: ExplicitContextTag2::from(OctetStringAsn1::from(as_rep_enc_data)), |
| 230 | + }), |
| 231 | + })) |
| 232 | +} |
0 commit comments