Skip to content

Commit cf91195

Browse files
authored
fix(kerberos): interoperate with MIT and Heimdal KDCs (#768)
## Summary Two client fixes for password logons against MIT and Heimdal KDCs, found while using `sspi` for SASL/GSSAPI binds to Active Directory and MIT/Heimdal realms. Each currently makes every such logon fail. - **Reuse the pre-authentication salt for AS-REP decryption.** MIT and Heimdal send PA-ETYPE-INFO2 only in `KDC_ERR_PREAUTH_REQUIRED`, not in the AS-REP. The client then derived the AS-REP key with an empty salt and failed with `Bad integrity: calculated hmac is different than provided`. The AS exchange now keeps the pre-authentication salt in the PA-DATA options, and the client falls back to it when the AS-REP has none. - **Accept an AS-REP enc-part tagged `EncTGSRepPart`.** The MIT KDC encodes the AS-REP enc-part with APPLICATION 26 for backwards compatibility, and the client failed with `Expected Application number tag 25 but got: 26`. It now accepts both tags. ### About the `EncTGSRepPart` fallback I know this fallback was removed during the review of #681 ("We should not expect `EncTgsRepPart` during AS exchange"). The MIT KDC however always sends it, so without the fallback no AS exchange with an MIT KDC can succeed: - MIT krb5, [`src/lib/krb5/asn.1/asn1_k_encode.c`](https://github.com/krb5/krb5/blob/master/src/lib/krb5/asn.1/asn1_k_encode.c): "For backwards compatibility, we encode both EncASRepPart and EncTGSRepPart with application tag 26. On decode, we accept either app tag". The encoder is `MAKE_ENCODER(encode_krb5_enc_kdc_rep_part, enc_tgs_rep_part)`, used for both reply types. - [RFC 4120 section 5.4.2](https://www.rfc-editor.org/rfc/rfc4120#section-5.4.2): "Some implementations unconditionally send an encrypted EncTGSRepPart (application tag number 26) in this field regardless of whether the reply is a AS-REP or a TGS-REP. In the interest of compatibility, implementors MAY relax the check on the tag number of the decrypted ENC-PART." The fallback only applies after the enc-part decrypted with the AS-REP key usage, and the TGS path is unchanged. ## Review and validation - The test `KdcMock` gets two options reproducing each KDC behavior (`omit_as_rep_etype_info2`, `tgs_rep_enc_part_in_as_rep`), each covered by an end-to-end client/server authentication test. Without the fix, each test fails with the error quoted above. - Also verified against real MIT and Heimdal KDCs (LDAP SASL/GSSAPI binds). - `cargo test -q -p sspi -p kdc --features network_client,__test-data` - `cargo check -q -p sspi --all-targets --features network_client,dns_resolver,scard,__test-data` (`--all-features` also enables `tsssp`, which only builds on Windows) - `cargo clippy -q -p sspi -p kdc --features network_client,__test-data --lib --tests -- -D warnings -A unreachable-pub -A clippy::non-ascii-literal` - `cargo clippy --manifest-path ffi/Cargo.toml -- -D warnings -D clippy::print_stdout` - `cargo fmt --all -- --check`
1 parent a738671 commit cf91195

5 files changed

Lines changed: 161 additions & 25 deletions

File tree

‎src/kerberos/client/extractors.rs‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,23 @@ fn decode_as_rep_session_key(as_rep: &AsRep, key: &[u8], enc_params: &Encryption
5858

5959
let enc_data = cipher.decrypt(key, AS_REP_ENC, &as_rep.0.enc_part.0.cipher.0.0)?;
6060

61-
// This function extracts the session key from an AS-REP, so the enc-part is
62-
// expected to be tagged EncASRepPart (APPLICATION 25). We do not accept
63-
// EncTGSRepPart here: that tag belongs to the TGS exchange.
64-
let as_rep_enc_part = picky_asn1_der::from_bytes::<EncAsRepPart>(&enc_data)?;
61+
// AS-REP specifies EncASRepPart (APPLICATION 25), but MIT uses EncTGSRepPart
62+
// (APPLICATION 26) for backwards compatibility, as some other implementations
63+
// do. RFC 4120 section 5.4.2 permits accepting both: "In the interest of
64+
// compatibility, implementors MAY relax the check on the tag number of the
65+
// decrypted ENC-PART."
66+
let key = match picky_asn1_der::from_bytes::<EncAsRepPart>(&enc_data) {
67+
Ok(as_rep_enc_part) => as_rep_enc_part.0.key.0.key_value.0.to_vec(),
68+
Err(_) => picky_asn1_der::from_bytes::<EncTgsRepPart>(&enc_data)?
69+
.0
70+
.key
71+
.0
72+
.key_value
73+
.0
74+
.to_vec(),
75+
};
6576

66-
Ok(as_rep_enc_part.0.key.0.key_value.0.to_vec().into())
77+
Ok(key.into())
6778
}
6879

6980
/// Extracts a session from the [AsRep].

‎src/kerberos/client/kdc/as_exchange.rs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{CredentialsBuffers, Error, ErrorKind, Kerberos, Result, Secret};
1414

1515
pub(crate) enum AsExchangeOutput {
1616
SendRequest(AsReq),
17-
Done(AsRep),
17+
Done(AsRep, String),
1818
}
1919

2020
#[derive(Debug, Clone, PartialEq)]
@@ -136,7 +136,12 @@ impl AsExchange {
136136
)?;
137137

138138
match as_rep {
139-
Ok(as_rep) => return Ok(AsExchangeOutput::Done(as_rep)),
139+
Ok(as_rep) => {
140+
return Ok(AsExchangeOutput::Done(
141+
as_rep,
142+
String::from_utf8_lossy(&self.salt).into_owned(),
143+
));
144+
}
140145
Err(err) if !self.retried_skew && err.0.error_code.0 == KRB_AP_ERR_SKEW => {
141146
client.clock_offset = clock_offset_from_error(&err, received_at)?;
142147
self.retried_skew = true;

‎src/kerberos/client/mod.rs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ pub enum KerberosClientState {
6060
},
6161
BeforeTgsExchange {
6262
as_rep: AsRep,
63+
preauth_salt: String,
6364
mech_id: ObjectIdentifier,
6465
tgt_ticket: Option<Ticket>,
6566
},
@@ -312,10 +313,11 @@ pub async fn initialize_security_context<'a>(
312313

313314
Some(SecurityStatus::ContinueNeeded)
314315
}
315-
AsExchangeOutput::Done(as_rep) => {
316+
AsExchangeOutput::Done(as_rep, preauth_salt) => {
316317
client.state =
317318
KerberosState::Client(Box::new(KerberosClientState::BeforeTgsExchange {
318319
as_rep,
320+
preauth_salt,
319321
mech_id,
320322
tgt_ticket,
321323
}));
@@ -327,22 +329,23 @@ pub async fn initialize_security_context<'a>(
327329
KdcResolution::KdcUrl(_) => {
328330
let mut as_response = Vec::new();
329331

330-
let as_rep = loop {
332+
let (as_rep, preauth_salt) = loop {
331333
let output = as_exchange.step(client, credentials, &as_response)?;
332334
match output {
333335
AsExchangeOutput::SendRequest(as_req) => {
334336
as_response =
335337
client.send(yield_point, &serialize_message(&as_req)?).await?;
336338
}
337-
AsExchangeOutput::Done(as_rep) => {
338-
break as_rep;
339+
AsExchangeOutput::Done(as_rep, preauth_salt) => {
340+
break (as_rep, preauth_salt);
339341
}
340342
}
341343
};
342344

343345
client.state =
344346
KerberosState::Client(Box::new(KerberosClientState::BeforeTgsExchange {
345347
as_rep,
348+
preauth_salt,
346349
mech_id,
347350
tgt_ticket,
348351
}));
@@ -353,6 +356,7 @@ pub async fn initialize_security_context<'a>(
353356
}
354357
KerberosClientState::BeforeTgsExchange {
355358
as_rep,
359+
preauth_salt,
356360
mech_id,
357361
tgt_ticket,
358362
} => {
@@ -361,6 +365,9 @@ pub async fn initialize_security_context<'a>(
361365
client.realm = Some(as_rep.0.crealm.0.to_string());
362366

363367
let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?;
368+
// Some KDCs omit PA-ETYPE-INFO2 from the AS-REP.
369+
// The AS-REP key uses the same salt as the pre-authentication key.
370+
let salt = if salt.is_empty() { preauth_salt } else { salt };
364371

365372
let encryption_type = CipherSuite::try_from(usize::from(encryption_type))?;
366373

‎tests/sspi/client_server/kerberos/kdc.rs‎

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ pub(crate) struct KdcMock {
109109
validators: Validators,
110110
clock_offset: Duration,
111111
reject_valid_preauth_with_skew: bool,
112+
/// Whether to omit PA-ETYPE-INFO2 from the AS-REP, as MIT and Heimdal KDCs do.
113+
omit_as_rep_etype_info2: bool,
114+
/// Whether the AS-REP enc-part is an EncTGSRepPart, as the MIT KDC encodes it.
115+
tgs_rep_enc_part_in_as_rep: bool,
112116
}
113117

114118
impl KdcMock {
@@ -134,6 +138,8 @@ impl KdcMock {
134138
validators,
135139
clock_offset: Duration::ZERO,
136140
reject_valid_preauth_with_skew: false,
141+
omit_as_rep_etype_info2: false,
142+
tgs_rep_enc_part_in_as_rep: false,
137143
}
138144
}
139145

@@ -147,6 +153,18 @@ impl KdcMock {
147153
self
148154
}
149155

156+
/// Sends the PA-ETYPE-INFO2 in the pre-authentication error only, not in the AS-REP.
157+
pub(crate) fn omit_as_rep_etype_info2(mut self) -> Self {
158+
self.omit_as_rep_etype_info2 = true;
159+
self
160+
}
161+
162+
/// Encodes the AS-REP enc-part as an EncTGSRepPart (APPLICATION 26).
163+
pub(crate) fn tgs_rep_enc_part_in_as_rep(mut self) -> Self {
164+
self.tgs_rep_enc_part_in_as_rep = true;
165+
self
166+
}
167+
150168
fn make_err<const ERROR_CODE: u32>(
151169
sname: PrincipalName,
152170
realm: Realm,
@@ -400,7 +418,7 @@ impl KdcMock {
400418

401419
let nonce = rng.try_next_u32().unwrap();
402420

403-
let as_rep_enc_part = EncAsRepPart::from(EncKdcRepPart {
421+
let enc_kdc_rep_part = EncKdcRepPart {
404422
key: ExplicitContextTag0::from(EncryptionKey {
405423
key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![AES256_ENC_TYPE])),
406424
key_value: ExplicitContextTag1::from(OctetStringAsn1::from(session_key.to_vec())),
@@ -422,19 +440,17 @@ impl KdcMock {
422440
sname: ExplicitContextTag10::from(sname.clone()),
423441
caddr: Optional::from(None),
424442
encrypted_pa_data: Optional::from(None),
425-
});
426-
let as_rep_enc_data = cipher
427-
.encrypt(
428-
&initial_key,
429-
AS_REP_ENC,
430-
&picky_asn1_der::to_vec(&as_rep_enc_part).unwrap(),
431-
)
432-
.unwrap();
443+
};
444+
let as_rep_enc_part = if self.tgs_rep_enc_part_in_as_rep {
445+
picky_asn1_der::to_vec(&EncTgsRepPart::from(enc_kdc_rep_part))
446+
} else {
447+
picky_asn1_der::to_vec(&EncAsRepPart::from(enc_kdc_rep_part))
448+
}
449+
.unwrap();
450+
let as_rep_enc_data = cipher.encrypt(&initial_key, AS_REP_ENC, &as_rep_enc_part).unwrap();
433451

434-
Ok(AsRep::from(KdcRep {
435-
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
436-
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AS_REP_MSG_TYPE])),
437-
padata: Optional::from(Some(ExplicitContextTag2::from(Asn1SequenceOf::from(vec![PaData {
452+
let etype_info2 = (!self.omit_as_rep_etype_info2).then(|| {
453+
ExplicitContextTag2::from(Asn1SequenceOf::from(vec![PaData {
438454
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ETYPE_INFO2_TYPE.to_vec())),
439455
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(
440456
picky_asn1_der::to_vec(&Asn1SequenceOf::from(vec![EtypeInfo2Entry {
@@ -446,7 +462,13 @@ impl KdcMock {
446462
}]))
447463
.unwrap(),
448464
)),
449-
}])))),
465+
}]))
466+
});
467+
468+
Ok(AsRep::from(KdcRep {
469+
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
470+
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AS_REP_MSG_TYPE])),
471+
padata: Optional::from(etype_info2),
450472
crealm: ExplicitContextTag3::from(realm.clone()),
451473
cname: ExplicitContextTag4::from(username.0.clone()),
452474
ticket: ExplicitContextTag5::from(Self::make_ticket(

‎tests/sspi/client_server/kerberos/mod.rs‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,97 @@ fn kerberos_auth() {
381381
);
382382
}
383383

384+
/// Authenticates the test user against a [KdcMock] configured by `configure_kdc`.
385+
fn kerberos_auth_with_kdc(configure_kdc: impl FnOnce(KdcMock) -> KdcMock) {
386+
let KrbEnvironment {
387+
realm,
388+
credentials,
389+
keys,
390+
users,
391+
target_name,
392+
target_service_name,
393+
} = init_krb_environment();
394+
395+
let ticket_decryption_key = keys[&UserName(target_service_name.clone())].clone();
396+
397+
let kdc = configure_kdc(KdcMock::new(
398+
realm,
399+
keys,
400+
users,
401+
Validators {
402+
as_req: Box::new(|_as_req| {}),
403+
tgs_req: Box::new(|_tgs_req| {}),
404+
},
405+
));
406+
let mut network_client = NetworkClientMock { kdc };
407+
408+
let client_config = KerberosConfig {
409+
kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())),
410+
client_computer_name: CLIENT_COMPUTER_NAME.into(),
411+
};
412+
let kerberos_client = Kerberos::new_client_from_config(client_config).unwrap();
413+
414+
let server_config = KerberosConfig {
415+
kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())),
416+
client_computer_name: SERVER_COMPUTER_NAME.into(),
417+
};
418+
let server_properties = ServerProperties {
419+
mech_types: MechTypeList::from(Vec::new()),
420+
max_time_skew: MAX_TIME_SKEW,
421+
ticket_decryption_key: Some(ticket_decryption_key.into()),
422+
service_name: target_service_name,
423+
additional_service_keys: Vec::new(),
424+
user: None,
425+
client: None,
426+
authenticators_cache: HashSet::new(),
427+
};
428+
let kerberos_server = Kerberos::new_server_from_config(server_config, server_properties).unwrap();
429+
430+
let credentials = CredentialsBuffers::try_from(credentials).unwrap();
431+
let mut client_credentials_handle = Some(credentials.clone());
432+
let mut server_credentials_handle = Some(credentials);
433+
434+
let client_flags = ClientRequestFlags::MUTUAL_AUTH
435+
| ClientRequestFlags::INTEGRITY
436+
| ClientRequestFlags::SEQUENCE_DETECT
437+
| ClientRequestFlags::REPLAY_DETECT
438+
| ClientRequestFlags::CONFIDENTIALITY;
439+
let server_flags = ServerRequestFlags::MUTUAL_AUTH
440+
| ServerRequestFlags::INTEGRITY
441+
| ServerRequestFlags::SEQUENCE_DETECT
442+
| ServerRequestFlags::REPLAY_DETECT
443+
| ServerRequestFlags::CONFIDENTIALITY;
444+
445+
run_kerberos(
446+
&mut SspiContext::Kerberos(kerberos_client),
447+
&mut client_credentials_handle,
448+
client_flags,
449+
&target_name,
450+
&mut SspiContext::Kerberos(kerberos_server),
451+
&mut server_credentials_handle,
452+
server_flags,
453+
&mut network_client,
454+
2,
455+
EmptySspiContextValidator,
456+
);
457+
}
458+
459+
/// MIT and Heimdal KDCs send PA-ETYPE-INFO2 only in the pre-authentication error.
460+
/// The AS-REP key uses the same salt as the pre-authentication key.
461+
#[test]
462+
fn kerberos_auth_without_as_rep_etype_info2() {
463+
kerberos_auth_with_kdc(KdcMock::omit_as_rep_etype_info2);
464+
}
465+
466+
/// MIT encodes the AS-REP enc-part as EncTGSRepPart, which clients may accept
467+
/// under [RFC 4120 section 5.4.2: KRB_KDC_REP Definition][kdc-rep].
468+
///
469+
/// [kdc-rep]: https://www.rfc-editor.org/rfc/rfc4120#section-5.4.2
470+
#[test]
471+
fn kerberos_auth_with_tgs_rep_enc_part_in_as_rep() {
472+
kerberos_auth_with_kdc(KdcMock::tgs_rep_enc_part_in_as_rep);
473+
}
474+
384475
#[test]
385476
fn kerberos_auth_recovers_from_kdc_clock_skew() {
386477
for clock_offset in [time::Duration::seconds(15), time::Duration::seconds(-15)] {

0 commit comments

Comments
 (0)