diff --git a/Cargo.lock b/Cargo.lock index 2abfacb1..b473e19d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1976,11 +1976,11 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "picky" -version = "7.0.0-rc.26" +version = "7.0.0-rc.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ad9686b270be4eb3f3c23027d202ad488a25c2bec6d6be20c2164b28d2c86c" +checksum = "3bcf32b73bfc151bc07c3fbf35e729ad3078d48c581f8656c0f62a529fbf681c" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "crypto-bigint", "crypto-common 0.2.2", "digest 0.11.3", @@ -2022,9 +2022,9 @@ dependencies = [ [[package]] name = "picky-asn1-der" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d413165e4bf7f808b9a27cbaba657657a2921f0965db833f488c4d4be96dcd2e" +checksum = "b4467118a2c640f4040aea27d01293620984b0b3f3ef0c53577c3bbf6bab4256" dependencies = [ "picky-asn1", "serde", @@ -2033,11 +2033,11 @@ dependencies = [ [[package]] name = "picky-asn1-x509" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d4117bd1b1dc5646359ee7243c50c5000c0920ea2d1fb120335a2f4c684b8" +checksum = "3f110226d163cea4380b0e5ef698a107061d198b279cc27d2b2c1e95c08cd9b0" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "crypto-bigint", "oid", "picky-asn1", @@ -2049,9 +2049,9 @@ dependencies = [ [[package]] name = "picky-krb" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d188f3192356068dbdba54bddbca6fd0f7a09565d3861eeb8efe1ab77ae8e97" +checksum = "e416cdfdfd3f05cddfa7bced8896fd760597616602a87b060776bedf4ce3cde5" dependencies = [ "aes", "block-padding", diff --git a/Cargo.toml b/Cargo.toml index 3604c15e..fb217dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,7 +115,7 @@ rand = "0.10" rand_core = "0.10" getrandom = "0.3" cfg-if = "1" -picky = { version = "=7.0.0-rc.26", default-features = false } +picky = { version = "=7.0.0-rc.27", default-features = false } sha1 = { version = "0.11", default-features = false } sha2 = "0.11" num-derive = "0.5" @@ -180,7 +180,7 @@ pkcs1 = "=0.8.0-rc.4" base64 = "0.23" static_assertions = "1" whoami = "2.1" -picky = { version = "=7.0.0-rc.26", default-features = false, features = ["x509"] } +picky = { version = "=7.0.0-rc.27", default-features = false, features = ["x509"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } proptest = "1.6" cfg-if = "1" diff --git a/crates/dpapi-web/src/lib.rs b/crates/dpapi-web/src/lib.rs index f44748f3..ebc3045a 100644 --- a/crates/dpapi-web/src/lib.rs +++ b/crates/dpapi-web/src/lib.rs @@ -16,7 +16,7 @@ use std::rc::Rc; use anyhow::Context; use dpapi::{CryptProtectSecretArgs, CryptUnprotectSecretArgs}; -use sspi::KerberosConfig; +use sspi::{KdcResolution, KerberosConfig}; use url::Url; use wasm_bindgen::prelude::*; @@ -169,7 +169,7 @@ impl DpapiConfig { let kerberos_config = Url::parse(kdc_proxy_url.unwrap_or_default().as_str()) .ok() .map(|url| KerberosConfig { - kdc_url: Some(url), + kdc_resolution: KdcResolution::KdcUrl(Some(url)), client_computer_name: computer_name.clone(), }); diff --git a/crates/winscard/Cargo.toml b/crates/winscard/Cargo.toml index e0dd3d6d..526d53a7 100644 --- a/crates/winscard/Cargo.toml +++ b/crates/winscard/Cargo.toml @@ -19,7 +19,7 @@ name = "winscard" bitflags = "2.11" iso7816 = "0.1" iso7816-tlv = "0.4" -picky = { version = "=7.0.0-rc.26", default-features = false, features = ["x509"] } +picky = { version = "=7.0.0-rc.27", default-features = false, features = ["x509"] } picky-asn1-x509 = "0.15" tracing = { version = "0.1", default-features = false, features = ["attributes"] } time = { version = "0.3", default-features = false, features = [ diff --git a/examples/kerberos.rs b/examples/kerberos.rs index 11afce41..961967ae 100644 --- a/examples/kerberos.rs +++ b/examples/kerberos.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Box> { .with(EnvFilter::from_env("SSPI_LOG_LEVEL")) .init(); - let kerberos_config = KerberosConfig::new(&kdc_url, hostname.clone()); + let kerberos_config = KerberosConfig::new_with_kdc_url(&kdc_url, hostname.clone()); let mut kerberos = Kerberos::new_client_from_config(kerberos_config).unwrap(); let mut acq_creds_handle_result = get_cred_handle(&mut kerberos, username, password); diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index ba5d8d14..cced9f76 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -29,7 +29,7 @@ sha1 = { version = "0.11", default-features = false } sha2 = "0.11" ffi-types = { workspace = true, features = ["sspi"] } -picky = { version = "=7.0.0-rc.26", default-features = false, features = ["x509"], optional = true } +picky = { version = "=7.0.0-rc.27", default-features = false, features = ["x509"], optional = true } picky-asn1-der = "0.5" picky-asn1 = { version = "0.10", optional = true } picky-asn1-x509 = { version = "0.15", optional = true } diff --git a/ffi/src/sspi/sec_handle.rs b/ffi/src/sspi/sec_handle.rs index 8e9ee117..8008760d 100644 --- a/ffi/src/sspi/sec_handle.rs +++ b/ffi/src/sspi/sec_handle.rs @@ -20,8 +20,8 @@ use sspi::kerberos::config::KerberosConfig; use sspi::ntlm::NtlmConfig; use sspi::{ CertContext, ClientRequestFlags, ConnectionInfo, Credentials, CredentialsBuffers, DataRepresentation, Error, - ErrorKind, Kerberos, Negotiate, NegotiateConfig, Ntlm, PackageInfo, Result, Secret, Sspi, SspiImpl, StreamSizes, - U16CString, Utf16String, Utf16StringExt, kerberos, negotiate, ntlm, pku2u, + ErrorKind, KdcResolution, Kerberos, Negotiate, NegotiateConfig, Ntlm, PackageInfo, Result, Secret, Sspi, SspiImpl, + StreamSizes, U16CString, Utf16String, Utf16StringExt, kerberos, negotiate, ntlm, pku2u, }; #[cfg(target_os = "windows")] use windows::Win32::Security::Cryptography::{ @@ -438,7 +438,7 @@ fn create_negotiate_context(attributes: &CredentialsAttributes) -> Result { - let krb_config = KerberosConfig{ + let krb_config = KerberosConfig { client_computer_name: try_execute!(hostname()), - kdc_url:None + kdc_resolution: KdcResolution::KdcUrl(None), }; SspiContext::Kerberos(try_execute!(Kerberos::new_client_from_config( krb_config diff --git a/src/auth_identity.rs b/src/auth_identity.rs index 474e672c..ca4cda82 100644 --- a/src/auth_identity.rs +++ b/src/auth_identity.rs @@ -733,6 +733,15 @@ impl CredentialsBuffers { _ => None, } } + + pub fn extract_password(&self) -> Secret { + Secret::new(match self { + CredentialsBuffers::AuthIdentity(auth_identity) => auth_identity.password.as_ref().as_ref().to_string(), + #[cfg(feature = "scard")] + CredentialsBuffers::SmartCard(smart_card) => smart_card.pin.as_ref().as_ref().to_string(), + CredentialsBuffers::Keytab(_keytab) => String::new(), + }) + } } /// Generic enum that encapsulates credentials for any type of authentication diff --git a/src/kerberos/client/as_exchange.rs b/src/kerberos/client/as_exchange.rs deleted file mode 100644 index c8b4ff70..00000000 --- a/src/kerberos/client/as_exchange.rs +++ /dev/null @@ -1,112 +0,0 @@ -use picky_krb::constants::error_codes::KRB_AP_ERR_SKEW; -use picky_krb::data_types::{KrbResult, ResultExt}; -use picky_krb::messages::{AsRep, KdcReqBody, KrbError}; -use time::{Duration, OffsetDateTime}; - -use crate::generator::YieldPointLocal; -use crate::kerberos::client::extractors::extract_salt_from_krb_error; -use crate::kerberos::client::generators::generate_as_req; -use crate::kerberos::pa_datas::AsReqPaDataOptions; -use crate::kerberos::utils::serialize_message; -use crate::{Error, ErrorKind, Kerberos, Result}; - -fn clock_offset_from_error(error: &KrbError, received_at: OffsetDateTime) -> Result { - let seconds = OffsetDateTime::try_from(error.0.stime.0.0.clone()) - .map_err(|_| Error::new(ErrorKind::InvalidToken, "KDC skew error has invalid server time"))?; - let usec = error.0.susec.0.0.as_slice(); - if usec.len() > 4 { - return Err(Error::new( - ErrorKind::InvalidToken, - "KDC skew error has invalid microseconds", - )); - } - let microseconds = usec.iter().fold(0_u32, |value, byte| (value << 8) | u32::from(*byte)); - if microseconds > 999_999 { - return Err(Error::new( - ErrorKind::InvalidToken, - "KDC skew error has invalid microseconds", - )); - } - let server_time = seconds - .checked_add(Duration::microseconds(i64::from(microseconds))) - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "KDC skew error has invalid server time"))?; - Ok(server_time - received_at) -} - -/// Performs the AS exchange as specified in [RFC 4120, section 3.1](https://www.rfc-editor.org/rfc/rfc4120#section-3.1). -/// -/// On a KDC clock-skew error, retries pre-authentication once using the server -/// time in the error. The offset is kept on the client context for subsequent -/// TGS and AP authenticators; other errors are returned without retrying. -pub(crate) async fn as_exchange( - client: &mut Kerberos, - yield_point: &mut YieldPointLocal, - kdc_req_body: &KdcReqBody, - mut pa_data_options: AsReqPaDataOptions<'_>, -) -> Result { - pa_data_options.with_pre_auth(false); - let pa_datas = pa_data_options.generate(client.current_kdc_time()?)?; - let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); - - let response = client.send(yield_point, &serialize_message(&as_req)?).await?; - - // first 4 bytes are message len. skipping them - { - let Some(response) = response.get(4..) else { - return Err(Error::new( - ErrorKind::InternalError, - "the KDC reply message is too small: expected at least 4 bytes", - )); - }; - - let mut d = picky_asn1_der::Deserializer::new_from_bytes(response); - let as_rep: KrbResult = KrbResult::deserialize(&mut d)?; - - if as_rep.is_ok() { - error!("KDC replied with AS_REP to the AS_REQ without the encrypted timestamp. The KRB_ERROR expected."); - - return Err(Error::new( - ErrorKind::InvalidToken, - "KDC server should not process AS_REQ without the pa-pac data", - )); - } - - if let Some(correct_salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? { - debug!("salt extracted successfully from the KRB_ERROR"); - - pa_data_options.with_salt(correct_salt.into_bytes()); - } - } - - pa_data_options.with_pre_auth(true); - let mut retried_skew = false; - loop { - let pa_datas = pa_data_options.generate(client.current_kdc_time()?)?; - let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); - let response = client.send(yield_point, &serialize_message(&as_req)?).await?; - let received_at = OffsetDateTime::now_utc(); - - // first 4 bytes are message len. skipping them - let Some(response) = response.get(4..) else { - return Err(Error::new( - ErrorKind::InternalError, - "the KDC reply message is too small: expected at least 4 bytes", - )); - }; - - let mut d = picky_asn1_der::Deserializer::new_from_bytes(response); - let as_rep: KrbResult = KrbResult::deserialize(&mut d)?; - match as_rep { - Ok(as_rep) => return Ok(as_rep), - Err(err) if !retried_skew && err.0.error_code.0 == KRB_AP_ERR_SKEW => { - client.clock_offset = clock_offset_from_error(&err, received_at)?; - retried_skew = true; - debug!(offset = ?client.clock_offset, "Retrying AS exchange with KDC clock offset"); - } - Err(err) => { - error!(?err, "AS exchange error"); - return Err(err.into()); - } - } - } -} diff --git a/src/kerberos/client/change_password.rs b/src/kerberos/client/change_password.rs index b1e5370f..03c33280 100644 --- a/src/kerberos/client/change_password.rs +++ b/src/kerberos/client/change_password.rs @@ -15,7 +15,7 @@ use crate::kerberos::client::generators::{ use crate::kerberos::client::principal::{get_client_principal_name_type, get_client_principal_realm}; use crate::kerberos::pa_datas::AsReqPaDataOptions; use crate::kerberos::utils::serialize_message; -use crate::kerberos::{CHANGE_PASSWORD_SERVICE_NAME, DEFAULT_ENCRYPTION_TYPE, KADMIN, client}; +use crate::kerberos::{CHANGE_PASSWORD_SERVICE_NAME, DEFAULT_ENCRYPTION_TYPE, KADMIN, server}; use crate::utils::generate_random_symmetric_key; use crate::{ClientRequestFlags, Error, ErrorKind, Kerberos, Result}; @@ -29,6 +29,13 @@ pub async fn change_password<'a>( yield_point: &mut YieldPointLocal, change_password: ChangePassword<'a>, ) -> Result<()> { + if client.is_iakerb() { + return Err(Error::new( + ErrorKind::UnsupportedFunction, + "Changing password is not supported with IAKerb.", + )); + } + let username = &change_password.account_name; let domain = &change_password.domain_name; let password = &change_password.old_password; @@ -59,7 +66,7 @@ pub async fn change_password<'a>( with_pre_auth: false, }); - let as_rep = client::as_exchange(client, yield_point, &kdc_req_body, pa_data_options).await?; + let as_rep = server::as_exchange::as_exchange(client, yield_point, &kdc_req_body, pa_data_options).await?; debug!("AS exchange finished successfully."); diff --git a/src/kerberos/client/extractors.rs b/src/kerberos/client/extractors.rs index d822a9c7..87f2b76b 100644 --- a/src/kerberos/client/extractors.rs +++ b/src/kerberos/client/extractors.rs @@ -3,6 +3,7 @@ use std::io::Read; use picky_asn1::wrapper::{Asn1SequenceOf, ObjectIdentifierAsn1}; use picky_asn1_der::Asn1RawDer; use picky_asn1_der::application_tag::ApplicationTag; +use picky_krb::constants::error_codes::KDC_ERR_PREAUTH_REQUIRED; use picky_krb::constants::key_usages::{AP_REP_ENC, AS_REP_ENC, KRB_PRIV_ENC_PART, TGS_REP_ENC_SESSION_KEY}; use picky_krb::constants::types::PA_ETYPE_INFO2_TYPE; use picky_krb::crypto::CipherSuite; @@ -23,6 +24,11 @@ use crate::{Error, ErrorKind, Result, Secret}; pub fn extract_salt_from_krb_error(error: &KrbError) -> Result> { trace!(?error, "KRB_ERROR"); + // Check if the error code is KDC_ERR_PREAUTH_REQUIRED. If not, propagate the KDC error. + if error.0.error_code.0 != KDC_ERR_PREAUTH_REQUIRED { + return Err(error.clone().into()); + } + if let Some(e_data) = error.0.e_data.0.as_ref() { let pa_datas: Asn1SequenceOf = picky_asn1_der::from_bytes(&e_data.0.0)?; diff --git a/src/kerberos/client/generators.rs b/src/kerberos/client/generators.rs index 06e1ccb5..95fd4090 100644 --- a/src/kerberos/client/generators.rs +++ b/src/kerberos/client/generators.rs @@ -652,7 +652,7 @@ pub(crate) fn generate_authenticator_at( sub_key, checksum, channel_bindings, - .. + extensions, } = options; let mut microseconds = current_date.microsecond(); @@ -689,6 +689,15 @@ pub(crate) fn generate_authenticator_at( // 4..19 - Channel binding information (19 inclusive). channel_binding_buf.copy_from_slice(&compute_md5_channel_bindings_hash(channel_bindings)?); } + + for extension in extensions { + // Although RFC 6542 specifies big-endian encoding for the extension type and length, + // Windows implementation uses little-endian encoding. + checksum_value.extend_from_slice(&extension.extension_type.to_le_bytes()); + checksum_value.extend_from_slice(&u32::try_from(extension.extension_value.len())?.to_le_bytes()); + checksum_value.extend_from_slice(&extension.extension_value); + } + Optional::from(Some(ExplicitContextTag3::from(Checksum { cksumtype: ExplicitContextTag0::from(IntegerAsn1::from(checksum_type)), checksum: ExplicitContextTag1::from(OctetStringAsn1::from(checksum_value)), diff --git a/src/kerberos/client/kdc/as_exchange.rs b/src/kerberos/client/kdc/as_exchange.rs new file mode 100644 index 00000000..d6a3fcff --- /dev/null +++ b/src/kerberos/client/kdc/as_exchange.rs @@ -0,0 +1,227 @@ +use picky_krb::constants::error_codes::KRB_AP_ERR_SKEW; +use picky_krb::data_types::KrbResult; +use picky_krb::messages::{AsRep, AsReq, KdcReqBody, KrbError}; +use time::{Duration, OffsetDateTime}; + +use crate::kerberos::EncryptionParams; +use crate::kerberos::client::extractors::extract_salt_from_krb_error; +use crate::kerberos::client::generators::{GenerateAsPaDataOptions, GenerateKeytabPaDataOptions, generate_as_req}; +use crate::kerberos::client::kdc::decode_kdc_reply; +use crate::kerberos::pa_datas::AsReqPaDataOptions; +#[cfg(feature = "scard")] +use crate::pk_init::DhParameters; +use crate::{CredentialsBuffers, Error, ErrorKind, Kerberos, Result, Secret}; + +pub(crate) enum AsExchangeOutput { + SendRequest(AsReq), + Done(AsRep), +} + +#[derive(Debug, Clone, PartialEq)] +enum AsExchangeState { + Initial, + PreauthRequiredErrorResponse, + AsRequest, + AsRepResponse, +} + +/// Performs the AS exchange as specified in [RFC 4120, section 3.1](https://www.rfc-editor.org/rfc/rfc4120#section-3.1). +/// +/// On a KDC clock-skew error, retries pre-authentication once using the server +/// time in the error. The offset is kept on the client context for subsequent +/// TGS and AP authenticators; other errors are returned without retrying. +#[derive(Debug, Clone, PartialEq)] +pub struct AsExchange { + state: AsExchangeState, + kdc_req_body: KdcReqBody, + retried_skew: bool, + // Instead of storing the `AsReqPaDataOptions` directly, we store its individual components (fields below). + // This design avoids introducing lifetime requirement that would require huge refactoring. + // The `AsReqPaDataOptions` is cheaply reconstructed at each step using the `build_pa_data_options` method. + password: Secret, + salt: Vec, + #[cfg(feature = "scard")] + dh_parameters: DhParameters, + #[cfg(feature = "scard")] + authenticator_nonce: u32, +} + +impl AsExchange { + pub(crate) fn new( + kdc_req_body: KdcReqBody, + password: Secret, + salt: Vec, + #[cfg(feature = "scard")] dh_parameters: DhParameters, + #[cfg(feature = "scard")] authenticator_nonce: u32, + ) -> Self { + Self { + state: AsExchangeState::Initial, + kdc_req_body, + retried_skew: false, + password, + salt, + #[cfg(feature = "scard")] + dh_parameters, + #[cfg(feature = "scard")] + authenticator_nonce, + } + } + + pub(crate) fn step( + &mut self, + client: &mut Kerberos, + credentials: &CredentialsBuffers, + response: &[u8], + ) -> Result { + loop { + match self.state { + AsExchangeState::Initial => { + let mut pa_data_options = self.build_pa_data_options(credentials, &client.encryption_params)?; + + pa_data_options.with_pre_auth(false); + let pa_datas = pa_data_options.generate(client.current_kdc_time()?)?; + let as_req = generate_as_req(pa_datas, self.kdc_req_body.clone()); + + self.state = AsExchangeState::PreauthRequiredErrorResponse; + return Ok(AsExchangeOutput::SendRequest(as_req)); + } + AsExchangeState::PreauthRequiredErrorResponse => { + let as_rep: KrbResult = decode_kdc_reply( + response, + client.is_iakerb(), + &mut client.iakerb_cookie, + &mut client.iakerb_gss_transcript, + )?; + + if as_rep.is_ok() { + error!( + "KDC replied with AS_REP to the AS_REQ without the encrypted timestamp. The KRB_ERROR expected." + ); + + return Err(Error::new( + ErrorKind::InvalidToken, + "KDC server should not process AS_REQ without the pa-pac data", + )); + } + + if let Some(salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? { + debug!("salt extracted successfully from the KRB_ERROR"); + self.salt = salt.into_bytes(); + } + + self.state = AsExchangeState::AsRequest; + + continue; + } + AsExchangeState::AsRequest => { + let mut pa_data_options = self.build_pa_data_options(credentials, &client.encryption_params)?; + + pa_data_options.with_pre_auth(true); + let pa_datas = pa_data_options.generate(client.current_kdc_time()?)?; + + self.state = AsExchangeState::AsRepResponse; + return Ok(AsExchangeOutput::SendRequest(generate_as_req( + pa_datas, + self.kdc_req_body.clone(), + ))); + } + AsExchangeState::AsRepResponse => { + let received_at = OffsetDateTime::now_utc(); + + let as_rep: KrbResult = decode_kdc_reply( + response, + client.is_iakerb(), + &mut client.iakerb_cookie, + &mut client.iakerb_gss_transcript, + )?; + + match as_rep { + Ok(as_rep) => return Ok(AsExchangeOutput::Done(as_rep)), + Err(err) if !self.retried_skew && err.0.error_code.0 == KRB_AP_ERR_SKEW => { + client.clock_offset = clock_offset_from_error(&err, received_at)?; + self.retried_skew = true; + debug!(offset = ?client.clock_offset, "Retrying AS exchange with KDC clock offset"); + + self.state = AsExchangeState::AsRequest; + continue; + } + Err(err) => { + error!(?err, "AS exchange error"); + return Err(err.into()); + } + } + } + } + } + } + + fn build_pa_data_options<'a>( + &'a mut self, + credentials: &'a CredentialsBuffers, + enc_params: &EncryptionParams, + ) -> Result> { + Ok(match credentials { + CredentialsBuffers::AuthIdentity(_) => AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions { + password: self.password.as_ref(), + salt: self.salt.clone(), + enc_params: enc_params.clone(), + with_pre_auth: false, + }), + CredentialsBuffers::Keytab(keytab) => AsReqPaDataOptions::Keytab(GenerateKeytabPaDataOptions { + key: keytab.key.clone(), + key_enctype: keytab.key_enctype.clone(), + with_pre_auth: false, + }), + #[cfg(feature = "scard")] + CredentialsBuffers::SmartCard(scard_identity_buffer) => { + use sha1::{Digest, Sha1}; + + use crate::smartcard::SmartCard; + use crate::{SmartCardIdentity, pk_init}; + + let scard_identity = SmartCardIdentity::try_from(scard_identity_buffer)?; + + let mut smart_card = SmartCard::from_credentials(&scard_identity)?; + let p2p_cert = scard_identity.certificate; + + AsReqPaDataOptions::SmartCard(Box::new(pk_init::GenerateAsPaDataOptions { + p2p_cert, + kdc_req_body: &self.kdc_req_body, + dh_parameters: self.dh_parameters.clone(), + sign_data: Box::new(move |data_to_sign| { + let mut sha1 = Sha1::new(); + sha1.update(data_to_sign); + let digest = sha1.finalize().to_vec(); + + smart_card.sign(digest) + }), + with_pre_auth: false, + authenticator_nonce: self.authenticator_nonce, + })) + } + }) + } +} + +pub(crate) fn clock_offset_from_error(error: &KrbError, received_at: OffsetDateTime) -> Result { + let seconds = OffsetDateTime::try_from(error.0.stime.0.0.clone()) + .map_err(|_| Error::new(ErrorKind::InvalidToken, "KDC skew error has invalid server time"))?; + let usec = error.0.susec.0.0.as_slice(); + if usec.len() > 4 { + return Err(Error::new( + ErrorKind::InvalidToken, + "KDC skew error has invalid microseconds", + )); + } + let microseconds = usec.iter().fold(0_u32, |value, byte| (value << 8) | u32::from(*byte)); + if microseconds > 999_999 { + return Err(Error::new( + ErrorKind::InvalidToken, + "KDC skew error has invalid microseconds", + )); + } + let server_time = seconds + .checked_add(Duration::microseconds(i64::from(microseconds))) + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "KDC skew error has invalid server time"))?; + Ok(server_time - received_at) +} diff --git a/src/kerberos/client/kdc/mod.rs b/src/kerberos/client/kdc/mod.rs new file mode 100644 index 00000000..ef33db3b --- /dev/null +++ b/src/kerberos/client/kdc/mod.rs @@ -0,0 +1,211 @@ +use picky_asn1_der::Asn1RawDer; +use picky_krb::data_types::{KrbResult, ResultExt}; +use picky_krb::gss_api::IAKerbProxyMessage; +use picky_krb::messages::IAKerbCookie; +use serde::de::DeserializeOwned; + +use crate::{Error, ErrorKind}; + +pub mod as_exchange; +pub mod tgs_exchange; + +fn decode_kdc_reply( + response: &[u8], + iakerb: bool, + iakerb_cookie: &mut IAKerbCookie, + iakerb_gss_transcript: &mut Vec, +) -> crate::Result> { + if response.is_empty() { + return Err(Error::new(ErrorKind::InternalError, "expected KDC reply message")); + } + + let krb_result = if iakerb { + let iakerb_proxy_msg = IAKerbProxyMessage::::decode_application_iakerb_proxy_message(response)?.0; + *iakerb_cookie = iakerb_proxy_msg.header.cookie.0; + iakerb_gss_transcript.extend_from_slice(response); + + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&iakerb_proxy_msg.krb_msg.0); + as ResultExt>::deserialize(&mut d)? + } else { + // first 4 bytes are message len. skipping them + if response.len() < 4 { + return Err(Error::new( + ErrorKind::InternalError, + "the KDC reply message is too small: expected at least 4 bytes", + )); + } + + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); + as ResultExt>::deserialize(&mut d)? + }; + + Ok(krb_result) +} + +#[cfg(test)] +mod tests { + use picky_asn1::restricted_string::Ia5String; + use picky_asn1::wrapper::{ + Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, + ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, GeneralStringAsn1, IntegerAsn1, OctetStringAsn1, + Optional, + }; + use picky_asn1_der::Asn1RawDer; + use picky_krb::data_types::{ + EncryptedData, KerberosStringAsn1, KrbResult, PaData, PrincipalName, Ticket, TicketInner, + }; + use picky_krb::messages::{AsRep, IAKerbCookie, KdcRep}; + + use crate::kerberos::client::kdc::decode_kdc_reply; + use crate::kerberos::messages::generate_iakerb_proxy_message; + + fn as_rep_raw() -> Vec { + vec![ + 107, 130, 2, 192, 48, 130, 2, 188, 160, 3, 2, 1, 5, 161, 3, 2, 1, 11, 162, 43, 48, 41, 48, 39, 161, 3, 2, + 1, 19, 162, 32, 4, 30, 48, 28, 48, 26, 160, 3, 2, 1, 18, 161, 19, 27, 17, 69, 88, 65, 77, 80, 76, 69, 46, + 67, 79, 77, 109, 121, 117, 115, 101, 114, 163, 13, 27, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, 79, 77, 164, + 19, 48, 17, 160, 3, 2, 1, 1, 161, 10, 48, 8, 27, 6, 109, 121, 117, 115, 101, 114, 165, 130, 1, 64, 97, 130, + 1, 60, 48, 130, 1, 56, 160, 3, 2, 1, 5, 161, 13, 27, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, 79, 77, 162, + 32, 48, 30, 160, 3, 2, 1, 2, 161, 23, 48, 21, 27, 6, 107, 114, 98, 116, 103, 116, 27, 11, 69, 88, 65, 77, + 80, 76, 69, 46, 67, 79, 77, 163, 129, 255, 48, 129, 252, 160, 3, 2, 1, 18, 161, 3, 2, 1, 1, 162, 129, 239, + 4, 129, 236, 229, 108, 127, 175, 235, 22, 11, 195, 254, 62, 101, 153, 38, 64, 83, 27, 109, 35, 253, 196, + 59, 21, 69, 124, 36, 145, 117, 98, 146, 80, 179, 3, 37, 191, 32, 69, 182, 19, 45, 245, 225, 205, 40, 33, + 245, 64, 96, 250, 167, 233, 4, 72, 222, 172, 23, 0, 66, 223, 108, 229, 56, 177, 9, 85, 252, 15, 249, 242, + 189, 240, 4, 45, 235, 72, 169, 207, 81, 60, 129, 61, 66, 191, 142, 254, 11, 231, 111, 219, 21, 155, 126, + 70, 20, 99, 169, 235, 134, 171, 70, 71, 238, 136, 156, 165, 46, 170, 53, 25, 233, 107, 78, 36, 141, 183, + 78, 123, 45, 239, 14, 239, 119, 178, 115, 146, 115, 93, 240, 130, 198, 225, 13, 175, 99, 71, 193, 252, 183, + 41, 77, 109, 158, 237, 159, 185, 164, 103, 132, 248, 223, 55, 201, 44, 74, 25, 130, 188, 76, 255, 128, 199, + 71, 137, 1, 154, 144, 17, 237, 167, 157, 123, 253, 150, 129, 189, 10, 121, 148, 70, 137, 249, 133, 43, 223, + 160, 250, 202, 175, 15, 6, 199, 177, 181, 237, 224, 226, 26, 230, 123, 219, 223, 164, 249, 206, 41, 40, 32, + 190, 14, 3, 196, 163, 41, 56, 118, 157, 114, 87, 233, 89, 178, 246, 74, 224, 43, 207, 53, 131, 32, 78, 111, + 114, 246, 153, 100, 110, 7, 166, 130, 1, 25, 48, 130, 1, 21, 160, 3, 2, 1, 18, 162, 130, 1, 12, 4, 130, 1, + 8, 14, 180, 181, 83, 180, 223, 85, 143, 123, 246, 189, 59, 97, 51, 73, 198, 5, 147, 87, 42, 240, 94, 250, + 203, 240, 45, 46, 190, 32, 135, 13, 24, 123, 127, 223, 30, 53, 200, 226, 164, 80, 207, 227, 34, 63, 139, 3, + 129, 240, 10, 193, 222, 123, 0, 64, 28, 232, 140, 63, 22, 143, 211, 114, 182, 138, 233, 103, 39, 233, 158, + 119, 215, 73, 227, 197, 80, 98, 48, 60, 62, 71, 207, 233, 144, 160, 28, 203, 79, 242, 40, 197, 224, 246, + 84, 9, 184, 188, 250, 231, 190, 97, 255, 41, 234, 238, 213, 203, 3, 192, 160, 220, 78, 78, 197, 45, 255, + 176, 13, 190, 245, 35, 208, 12, 80, 93, 81, 65, 252, 199, 184, 202, 197, 95, 49, 179, 237, 64, 116, 52, + 220, 109, 123, 202, 78, 63, 146, 121, 178, 168, 157, 84, 80, 246, 250, 75, 69, 93, 184, 48, 115, 32, 139, + 4, 90, 164, 30, 208, 100, 37, 220, 168, 165, 2, 224, 124, 102, 164, 130, 34, 66, 134, 131, 16, 7, 206, 32, + 138, 30, 217, 225, 125, 69, 82, 78, 127, 73, 216, 235, 130, 159, 41, 23, 28, 197, 19, 39, 207, 144, 160, + 197, 11, 85, 39, 102, 167, 237, 83, 132, 78, 165, 215, 173, 61, 90, 113, 215, 201, 213, 158, 19, 190, 68, + 135, 94, 136, 63, 105, 119, 225, 127, 193, 148, 33, 74, 41, 154, 68, 104, 52, 227, 188, 19, 62, 26, 55, 15, + 20, 53, 221, 200, 137, 197, 2, 243, + ] + } + + fn as_rep() -> AsRep { + AsRep::from(KdcRep { + pvno: ExplicitContextTag0::from(IntegerAsn1(vec![5])), + msg_type: ExplicitContextTag1::from(IntegerAsn1(vec![11])), + padata: Optional::from(Some(ExplicitContextTag2::from(Asn1SequenceOf::from(vec![PaData { + padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![19])), + padata_data: ExplicitContextTag2::from(OctetStringAsn1(vec![ + 48, 28, 48, 26, 160, 3, 2, 1, 18, 161, 19, 27, 17, 69, 88, 65, 77, 80, 76, 69, 46, 67, 79, 77, 109, + 121, 117, 115, 101, 114, + ])), + }])))), + crealm: ExplicitContextTag3::from(GeneralStringAsn1::from( + Ia5String::from_string("EXAMPLE.COM".to_owned()).unwrap(), + )), + cname: ExplicitContextTag4::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1(vec![1])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![GeneralStringAsn1::from( + Ia5String::from_string("myuser".to_owned()).unwrap(), + )])), + }), + ticket: ExplicitContextTag5::from(Ticket::from(TicketInner { + tkt_vno: ExplicitContextTag0::from(IntegerAsn1(vec![5])), + realm: ExplicitContextTag1::from(GeneralStringAsn1::from( + Ia5String::from_string("EXAMPLE.COM".to_owned()).unwrap(), + )), + sname: ExplicitContextTag2::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1(vec![2])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![ + KerberosStringAsn1::from(Ia5String::from_string("krbtgt".to_owned()).unwrap()), + KerberosStringAsn1::from(Ia5String::from_string("EXAMPLE.COM".to_owned()).unwrap()), + ])), + }), + enc_part: ExplicitContextTag3::from(EncryptedData { + etype: ExplicitContextTag0::from(IntegerAsn1(vec![18])), + kvno: Optional::from(Some(ExplicitContextTag1::from(IntegerAsn1(vec![1])))), + cipher: ExplicitContextTag2::from(OctetStringAsn1::from(vec![ + 229, 108, 127, 175, 235, 22, 11, 195, 254, 62, 101, 153, 38, 64, 83, 27, 109, 35, 253, 196, 59, + 21, 69, 124, 36, 145, 117, 98, 146, 80, 179, 3, 37, 191, 32, 69, 182, 19, 45, 245, 225, 205, + 40, 33, 245, 64, 96, 250, 167, 233, 4, 72, 222, 172, 23, 0, 66, 223, 108, 229, 56, 177, 9, 85, + 252, 15, 249, 242, 189, 240, 4, 45, 235, 72, 169, 207, 81, 60, 129, 61, 66, 191, 142, 254, 11, + 231, 111, 219, 21, 155, 126, 70, 20, 99, 169, 235, 134, 171, 70, 71, 238, 136, 156, 165, 46, + 170, 53, 25, 233, 107, 78, 36, 141, 183, 78, 123, 45, 239, 14, 239, 119, 178, 115, 146, 115, + 93, 240, 130, 198, 225, 13, 175, 99, 71, 193, 252, 183, 41, 77, 109, 158, 237, 159, 185, 164, + 103, 132, 248, 223, 55, 201, 44, 74, 25, 130, 188, 76, 255, 128, 199, 71, 137, 1, 154, 144, 17, + 237, 167, 157, 123, 253, 150, 129, 189, 10, 121, 148, 70, 137, 249, 133, 43, 223, 160, 250, + 202, 175, 15, 6, 199, 177, 181, 237, 224, 226, 26, 230, 123, 219, 223, 164, 249, 206, 41, 40, + 32, 190, 14, 3, 196, 163, 41, 56, 118, 157, 114, 87, 233, 89, 178, 246, 74, 224, 43, 207, 53, + 131, 32, 78, 111, 114, 246, 153, 100, 110, 7, + ])), + }), + })), + enc_part: ExplicitContextTag6::from(EncryptedData { + etype: ExplicitContextTag0::from(IntegerAsn1(vec![18])), + kvno: Optional::from(None), + cipher: ExplicitContextTag2::from(OctetStringAsn1::from(vec![ + 14, 180, 181, 83, 180, 223, 85, 143, 123, 246, 189, 59, 97, 51, 73, 198, 5, 147, 87, 42, 240, 94, + 250, 203, 240, 45, 46, 190, 32, 135, 13, 24, 123, 127, 223, 30, 53, 200, 226, 164, 80, 207, 227, + 34, 63, 139, 3, 129, 240, 10, 193, 222, 123, 0, 64, 28, 232, 140, 63, 22, 143, 211, 114, 182, 138, + 233, 103, 39, 233, 158, 119, 215, 73, 227, 197, 80, 98, 48, 60, 62, 71, 207, 233, 144, 160, 28, + 203, 79, 242, 40, 197, 224, 246, 84, 9, 184, 188, 250, 231, 190, 97, 255, 41, 234, 238, 213, 203, + 3, 192, 160, 220, 78, 78, 197, 45, 255, 176, 13, 190, 245, 35, 208, 12, 80, 93, 81, 65, 252, 199, + 184, 202, 197, 95, 49, 179, 237, 64, 116, 52, 220, 109, 123, 202, 78, 63, 146, 121, 178, 168, 157, + 84, 80, 246, 250, 75, 69, 93, 184, 48, 115, 32, 139, 4, 90, 164, 30, 208, 100, 37, 220, 168, 165, + 2, 224, 124, 102, 164, 130, 34, 66, 134, 131, 16, 7, 206, 32, 138, 30, 217, 225, 125, 69, 82, 78, + 127, 73, 216, 235, 130, 159, 41, 23, 28, 197, 19, 39, 207, 144, 160, 197, 11, 85, 39, 102, 167, + 237, 83, 132, 78, 165, 215, 173, 61, 90, 113, 215, 201, 213, 158, 19, 190, 68, 135, 94, 136, 63, + 105, 119, 225, 127, 193, 148, 33, 74, 41, 154, 68, 104, 52, 227, 188, 19, 62, 26, 55, 15, 20, 53, + 221, 200, 137, 197, 2, 243, + ])), + }), + }) + } + + #[test] + fn decode_kdc_reply_from_iakerb_proxy() { + let iakerb = true; + let mut iakerb_cookie = IAKerbCookie::default(); + let mut iakerb_gss_transcript = Vec::new(); + + let expected_cookie = Some(ExplicitContextTag2::from(OctetStringAsn1::from(vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + ]))); + let kdc_reply_raw = generate_iakerb_proxy_message(expected_cookie.clone(), Asn1RawDer(as_rep_raw())).unwrap(); + + let kdc_reply: KrbResult = + decode_kdc_reply(&kdc_reply_raw, iakerb, &mut iakerb_cookie, &mut iakerb_gss_transcript).unwrap(); + + assert_eq!(kdc_reply, Ok(as_rep())); + assert_eq!(iakerb_cookie, expected_cookie); + assert_eq!(iakerb_gss_transcript, kdc_reply_raw); + } + + #[test] + fn decode_kdc_reply_from_external_kdc() { + let iakerb = false; + let iakerb_cookie = IAKerbCookie::default(); + let iakerb_gss_transcript = Vec::new(); + + // Placeholder for message length. It is not validated, so it can be any value. + let mut kdc_reply_raw = vec![0; 4]; + kdc_reply_raw.extend_from_slice(&as_rep_raw()); + + let kdc_reply: KrbResult = decode_kdc_reply( + &kdc_reply_raw, + iakerb, + &mut iakerb_cookie.clone(), + &mut iakerb_gss_transcript.clone(), + ) + .unwrap(); + + assert_eq!(kdc_reply, Ok(as_rep())); + assert_eq!(iakerb_cookie, None); + assert_eq!(iakerb_gss_transcript, Vec::new()); + } +} diff --git a/src/kerberos/client/kdc/tgs_exchange.rs b/src/kerberos/client/kdc/tgs_exchange.rs new file mode 100644 index 00000000..1b12b580 --- /dev/null +++ b/src/kerberos/client/kdc/tgs_exchange.rs @@ -0,0 +1,171 @@ +use hmac::digest::common::getrandom::SysRng; +use picky_krb::data_types::Ticket; +use picky_krb::messages::{KdcRep, TgsRep, TgsReq}; +use rand::prelude::StdRng; +use rand_core::{Rng, SeedableRng}; + +use crate::kerberos::client::extractors::extract_session_key_from_tgs_rep; +use crate::kerberos::client::generators::{ + GenerateAuthenticatorOptions, GenerateTgsReqOptions, generate_authenticator_at, generate_tgs_req, +}; +use crate::kerberos::client::kdc::decode_kdc_reply; +use crate::kerberos::client::referral_target_realm; +use crate::{ClientRequestFlags, Error, ErrorKind, Kerberos, Result, Secret}; + +const MAX_REFERRAL_HOPS: usize = 10; + +pub(crate) enum TgsExchangeOutput<'a> { + SendRequest((TgsReq, &'a str)), + Done((TgsRep, Secret>)), +} + +#[derive(Debug, Clone, PartialEq)] +enum TgsExchangeState { + TgsRequest, + TgsResponse, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TgsExchange { + state: TgsExchangeState, + realm: String, + ticket: Option, + tgt_session_key: Secret>, + auth_rep: KdcRep, + additional_tickets: Option>, + hops: usize, + context_requirements: ClientRequestFlags, +} + +impl TgsExchange { + pub(crate) fn new( + realm: String, + ticket: Ticket, + tgt_session_key: Secret>, + auth_rep: KdcRep, + additional_tickets: Option>, + context_requirements: ClientRequestFlags, + ) -> Self { + Self { + state: TgsExchangeState::TgsRequest, + realm, + ticket: Some(ticket), + tgt_session_key, + auth_rep, + additional_tickets, + hops: 0, + context_requirements, + } + } + + pub(crate) fn step<'a>( + &'a mut self, + client: &mut Kerberos, + service_principal: &str, + response: &[u8], + ) -> Result> { + // Cross-realm referral chasing (RFC 4120 §3.3.3.2 / MS-KILE). + // + // * [Cross-Realm Operation](https://www.rfc-editor.org/rfc/rfc4120.html#section-1.2) + // * [Server Referrals](https://www.rfc-editor.org/rfc/rfc6806.html#section-8) + // + // A KDC can only issue tickets for principals in its own realm. When the requested + // service lives in another realm (e.g. a user in `RJM.LOCAL` targeting a host in the + // child realm `DEV.RJM.LOCAL`), the KDC does not return the service ticket. Instead it + // returns a referral TGT whose `sname` is `krbtgt/`, and the client must + // re-issue the TGS-REQ for the same service to ``'s KDC using that referral + // TGT. We loop until the returned ticket's `sname` matches the requested service (i.e. + // it is no longer a `krbtgt/...` referral). + // + // The referral hop is routed via `send_for_realm`, which resolves the target realm's + // KDC through `SSPI_KDC_URL_` (env) / krb5.conf / DNS SRV rather than the pinned + // home-realm KDC, which cannot decrypt a `krbtgt/` referral ticket. + // + // NOTE: this referral-chasing branch is not exercised under IAKerb. The LocalKDC returns + // the final service ticket directly, so `referral_target_realm` never observes `sname` of + // the form `krbtgt/` and the TGS exchange always finishes on the first response. + loop { + match self.state { + TgsExchangeState::TgsRequest => { + let mut rand = StdRng::try_from_rng(&mut SysRng)?; + + let now = client.current_kdc_time()?; + let mut authenticator = generate_authenticator_at( + GenerateAuthenticatorOptions { + kdc_rep: &self.auth_rep, + seq_num: Some(rand.next_u32()), + sub_key: None, + checksum: None, + channel_bindings: client.channel_bindings.as_ref(), + extensions: Vec::new(), + }, + now, + )?; + + let tgs_req = generate_tgs_req(GenerateTgsReqOptions { + realm: &self.realm, + service_principal, + session_key: &self.tgt_session_key, + ticket: self + .ticket + .take() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "ticket is missing"))?, + authenticator: &mut authenticator, + additional_tickets: self.additional_tickets.take(), + enc_params: &client.encryption_params, + context_requirements: self.context_requirements, + })?; + + self.state = TgsExchangeState::TgsResponse; + + return Ok(TgsExchangeOutput::SendRequest((tgs_req, &self.realm))); + } + TgsExchangeState::TgsResponse => { + if response.is_empty() { + return Err(Error::new(ErrorKind::InternalError, "the KDC reply message is absent")); + } + + let tgs_rep = decode_kdc_reply( + response, + client.is_iakerb(), + &mut client.iakerb_cookie, + &mut client.iakerb_gss_transcript, + )?; + let tgs_rep = tgs_rep.inspect_err(|err| error!(?err, "TGS exchange error"))?; + + let session_key = + extract_session_key_from_tgs_rep(&tgs_rep, &self.tgt_session_key, &client.encryption_params)?; + + // A referral TGT is identified by an `sname` of the form `krbtgt/`. + let Some(next_realm) = referral_target_realm(&tgs_rep.0.ticket.0.0.sname.0) else { + debug!("TGS exchange finished successfully"); + return Ok(TgsExchangeOutput::Done((tgs_rep, session_key))); + }; + debug!(%self.realm, %next_realm, "Received cross-realm referral TGT; chasing referral"); + + self.hops += 1; + if self.hops >= MAX_REFERRAL_HOPS { + return Err(Error::new( + ErrorKind::NoAuthenticatingAuthority, + format!( + "exceeded maximum Kerberos referral hops ({MAX_REFERRAL_HOPS}) resolving {service_principal}" + ), + )); + } + if next_realm.eq_ignore_ascii_case(&self.realm) { + return Err(Error::new( + ErrorKind::NoAuthenticatingAuthority, + format!("Kerberos referral did not progress past realm `{}`", self.realm), + )); + } + + self.ticket = Some(tgs_rep.0.ticket.0.clone()); + self.tgt_session_key = session_key; + self.auth_rep = tgs_rep.0; + self.realm = next_realm; + self.state = TgsExchangeState::TgsRequest; + } + } + } + } +} diff --git a/src/kerberos/client/mod.rs b/src/kerberos/client/mod.rs index c5d737a2..e70d757f 100644 --- a/src/kerberos/client/mod.rs +++ b/src/kerberos/client/mod.rs @@ -1,46 +1,82 @@ -mod as_exchange; mod change_password; pub mod extractors; pub mod generators; +pub mod kdc; pub mod principal; use std::io::Write; -pub(crate) use as_exchange::as_exchange; pub use change_password::change_password; +use oid::ObjectIdentifier; use picky_asn1_x509::oids; use picky_krb::constants::gss_api::{AP_REP_TOKEN_ID, AP_REQ_TOKEN_ID, AUTHENTICATOR_CHECKSUM_TYPE, TGT_REQ_TOKEN_ID}; use picky_krb::crypto::CipherSuite; -use picky_krb::data_types::{KrbResult, PrincipalName, ResultExt}; -use picky_krb::messages::{ApRep, TgsRep}; +use picky_krb::data_types::{PrincipalName, Ticket}; +use picky_krb::messages::{ApRep, AsRep, TgsRep}; use rand::rngs::{StdRng, SysRng}; -use rand_core::{Rng as _, SeedableRng as _}; +use rand_core::SeedableRng; use self::extractors::{ decrypt_ap_rep, extract_encryption_params_from_as_rep, extract_seq_number_from_ap_rep, - extract_session_key_from_tgs_rep, extract_sub_session_key_from_ap_rep, extract_tgt_ticket_with_oid, + extract_sub_session_key_from_ap_rep, extract_tgt_ticket_with_oid, }; use self::generators::{ - ChecksumOptions, ChecksumValues, EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, - GenerateAuthenticatorOptions, GenerateKeytabPaDataOptions, GenerateTgsReqOptions, GssFlags, generate_ap_rep, - generate_ap_req, generate_as_req_kdc_body, generate_authenticator_at, generate_nonce, generate_tgs_req, + ChecksumOptions, ChecksumValues, EncKey, GenerateAsReqOptions, GenerateAuthenticatorOptions, GssFlags, + generate_ap_rep, generate_ap_req, generate_as_req_kdc_body, generate_authenticator_at, generate_nonce, + generate_tgt_req, }; use self::principal::{ ClientPrincipalName, get_client_principal_name, get_client_principal_name_type, get_client_principal_realm, }; use crate::channel_bindings::ChannelBindings; use crate::generator::YieldPointLocal; -use crate::kerberos::client::generators::generate_tgt_req; -use crate::kerberos::messages::{decode_krb_message, generate_krb_message}; -use crate::kerberos::pa_datas::{AsRepSessionKeyExtractor, AsReqPaDataOptions}; +use crate::kerberos::client::kdc::as_exchange::{AsExchange, AsExchangeOutput}; +use crate::kerberos::client::kdc::tgs_exchange::{TgsExchange, TgsExchangeOutput}; +use crate::kerberos::config::KdcResolution; +use crate::kerberos::messages::{decode_krb_message, generate_iakerb_proxy_message, generate_krb_message}; +use crate::kerberos::pa_datas::AsRepSessionKeyExtractor; use crate::kerberos::utils::serialize_message; use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EC, TGT_SERVICE_NAME}; +use crate::pku2u::generate_authenticator_extension; use crate::utils::{generate_random_symmetric_key, parse_target_name}; use crate::{ BufferType, ClientRequestFlags, ClientResponseFlags, CredentialsBuffers, Error, ErrorKind, - InitializeSecurityContextResult, Kerberos, KerberosState, Result, SecurityBuffer, SecurityStatus, SspiImpl, + InitializeSecurityContextResult, Kerberos, KerberosState, Result, Secret, SecurityBuffer, SecurityStatus, SspiImpl, }; +#[allow( + clippy::enum_variant_names, + reason = "the `Exchange` postfix is meaningful for these variant names" +)] +#[derive(Debug, Default, Clone, PartialEq)] +pub enum KerberosClientState { + #[default] + TgtExchange, + BeforeAsExchange, + AsExchange { + as_exchange: AsExchange, + mech_id: ObjectIdentifier, + tgt_ticket: Option, + }, + BeforeTgsExchange { + as_rep: AsRep, + mech_id: ObjectIdentifier, + tgt_ticket: Option, + }, + TgsExchange { + tgs_exchange: TgsExchange, + context_requirements: ClientRequestFlags, + mech_id: ObjectIdentifier, + }, + BeforeApExchange { + tgs_rep: TgsRep, + session_key: Secret>, + context_requirements: ClientRequestFlags, + mech_id: ObjectIdentifier, + }, + ApExchange, +} + /// Inspects the `sname` of a ticket returned in a TGS-REP to decide whether it is a cross-realm /// referral TGT rather than the requested service ticket. /// @@ -70,481 +106,592 @@ pub async fn initialize_security_context<'a>( ) -> Result { trace!(?builder); - if let KerberosState::TgtExchange = client.state { - if builder + // When the machine is not domain-joined, Kerberos cannot use the LocalKDC for TGT exchange + // and attempts to locate an external KDC as a fallback. Since no external KDC is available, + // the server returns `STATUS_NO_LOGON_SERVERS` error. + // Therefore, if IAKerb is enabled, we must disable the Kerberos U2U extension (`USE_SESSION_KEY` flag). + if client.is_iakerb() + && builder .context_requirements .contains(ClientRequestFlags::USE_SESSION_KEY) - { - client.krb5_user_to_user = true; - - let (service_name, service_principal_name) = parse_target_name(builder.target_name.ok_or_else(|| { - Error::new( - ErrorKind::NoCredentials, - "Service target name (service principal name) is not provided", - ) - })?)?; - - let tgt_req = generate_tgt_req(&[service_name, service_principal_name])?; - - let encoded_neg_tgt_req = if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { - generate_krb_message(oids::krb5_user_to_user(), TGT_REQ_TOKEN_ID, tgt_req)? - } else { - // Do not wrap if the `USE_DCE_STYLE` flag is set. - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 - picky_asn1_der::to_vec(&tgt_req)? - }; - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - output_token.buffer = encoded_neg_tgt_req; + { + warn!("IAKerb is enabled and the USE_SESSION_KEY flag is set. Forcibly turning it off..."); + builder.context_requirements.remove(ClientRequestFlags::USE_SESSION_KEY); + } - client.state = KerberosState::Preauthentication; + loop { + let status = match core::mem::take(&mut client.state) { + KerberosState::Client(state) => { + match *state { + KerberosClientState::TgtExchange => { + if builder + .context_requirements + .contains(ClientRequestFlags::USE_SESSION_KEY) + { + client.krb5_user_to_user = true; + + let (service_name, service_principal_name) = + parse_target_name(builder.target_name.ok_or_else(|| { + Error::new( + ErrorKind::NoCredentials, + "Service target name (service principal name) is not provided", + ) + })?)?; + + debug!( + ?service_name, + ?service_principal_name, + "target_name = {:?}", + builder.target_name + ); + + let tgt_req = generate_tgt_req(&[service_name, service_principal_name])?; + + let encoded_neg_tgt_req = + if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { + generate_krb_message(oids::krb5_user_to_user(), TGT_REQ_TOKEN_ID, tgt_req)? + } else { + // Do not wrap if the `USE_DCE_STYLE` flag is set. + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 + picky_asn1_der::to_vec(&tgt_req)? + }; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer = encoded_neg_tgt_req; + + client.state = KerberosState::Client(Box::new(KerberosClientState::BeforeAsExchange)); + + Some(SecurityStatus::ContinueNeeded) + } else { + client.state = KerberosState::Client(Box::new(KerberosClientState::BeforeAsExchange)); + + None + } + } + KerberosClientState::BeforeAsExchange => { + let input = builder + .input + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?; + + if let Ok(sec_buffer) = SecurityBuffer::find_buffer(input, BufferType::ChannelBindings) { + client.channel_bindings = Some(ChannelBindings::from_bytes(&sec_buffer.buffer)?); + } + + let input_token = SecurityBuffer::find_buffer(input, BufferType::Token) + .map(|security_buffer| security_buffer.buffer.as_slice()) + .unwrap_or_default(); + + let (tgt_ticket, mech_id) = + if let Some((tbt_ticket, mech_oid)) = extract_tgt_ticket_with_oid(input_token)? { + (Some(tbt_ticket), mech_oid.0) + } else { + (None, oids::krb5()) + }; + client.krb5_user_to_user = mech_id == oids::krb5_user_to_user(); + + let credentials = + builder.credentials_handle.as_ref().unwrap().as_ref().ok_or_else(|| { + Error::new(ErrorKind::WrongCredentialHandle, "No credentials provided") + })?; + + let (username, realm, cname_type) = match credentials { + CredentialsBuffers::AuthIdentity(auth_identity) => { + let username = auth_identity.user.to_string(); + let domain = auth_identity.domain.to_string(); + + let realm = get_client_principal_realm(&username, &domain); + let cname_type = get_client_principal_name_type(&username, &domain); + + (username, realm, cname_type) + } + #[cfg(feature = "scard")] + CredentialsBuffers::SmartCard(smart_card) => { + let username = smart_card.username.to_string(); + + let realm = get_client_principal_realm(&username, ""); + let cname_type = get_client_principal_name_type(&username, ""); + + (username, realm.to_uppercase(), cname_type) + } + CredentialsBuffers::Keytab(keytab) => { + // The name type is read off the principal's user name format explicitly. + let ClientPrincipalName { + name, + realm_domain, + name_type, + } = get_client_principal_name(&keytab.principal); + + let realm = get_client_principal_realm(name, realm_domain); + + (name.to_owned(), realm, name_type) + } + }; + + client.realm = Some(realm.clone()); + + let mut rand = StdRng::try_from_rng(&mut SysRng)?; + let options = GenerateAsReqOptions { + realm: &realm, + username: &username, + cname_type, + snames: &[TGT_SERVICE_NAME, &realm], + nonce: generate_nonce(&mut rand), + hostname: &client.config.client_computer_name, + context_requirements: builder.context_requirements, + }; + let kdc_req_body = generate_as_req_kdc_body(&options)?; + + let salt = match credentials { + CredentialsBuffers::AuthIdentity(auth_identity) => { + let domain = auth_identity.domain.to_string(); + format!("{domain}{username}").into_bytes() + } + _ => Vec::new(), + }; + + #[cfg(feature = "scard")] + { + use crate::pku2u::generate_client_dh_parameters; + + client.dh_parameters = Some(generate_client_dh_parameters(&mut rand)); + } + + let as_exchange = AsExchange::new( + kdc_req_body, + credentials.extract_password(), + salt, + #[cfg(feature = "scard")] + client.dh_parameters.clone().expect("DH parameters are set above"), + #[cfg(feature = "scard")] + generate_nonce(&mut rand), + ); + + client.state = KerberosState::Client(Box::new(KerberosClientState::AsExchange { + as_exchange, + mech_id, + tgt_ticket, + })); + + None + } + KerberosClientState::AsExchange { + mut as_exchange, + mech_id, + tgt_ticket, + } => { + let credentials = + builder.credentials_handle.as_ref().unwrap().as_ref().ok_or_else(|| { + Error::new(ErrorKind::WrongCredentialHandle, "No credentials provided") + })?; + + match client.config.kdc_resolution { + KdcResolution::IAKerb => { + let input = builder.input.as_ref().ok_or_else(|| { + Error::new(ErrorKind::InvalidToken, "input buffers must be specified") + })?; + + let input_token = SecurityBuffer::find_buffer(input, BufferType::Token) + .map(|security_buffer| security_buffer.buffer.as_slice()) + .unwrap_or_default(); + + match as_exchange.step(client, credentials, input_token)? { + AsExchangeOutput::SendRequest(as_req) => { + let output_token = + SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer = + generate_iakerb_proxy_message(client.iakerb_cookie.take(), as_req)?; + client.iakerb_gss_transcript.extend_from_slice(&output_token.buffer); + + client.state = + KerberosState::Client(Box::new(KerberosClientState::AsExchange { + as_exchange, + mech_id, + tgt_ticket, + })); + + Some(SecurityStatus::ContinueNeeded) + } + AsExchangeOutput::Done(as_rep) => { + client.state = + KerberosState::Client(Box::new(KerberosClientState::BeforeTgsExchange { + as_rep, + mech_id, + tgt_ticket, + })); + + None + } + } + } + KdcResolution::KdcUrl(_) => { + let mut as_response = Vec::new(); + + let as_rep = loop { + let output = as_exchange.step(client, credentials, &as_response)?; + match output { + AsExchangeOutput::SendRequest(as_req) => { + as_response = + client.send(yield_point, &serialize_message(&as_req)?).await?; + } + AsExchangeOutput::Done(as_rep) => { + break as_rep; + } + } + }; + + client.state = + KerberosState::Client(Box::new(KerberosClientState::BeforeTgsExchange { + as_rep, + mech_id, + tgt_ticket, + })); + + None + } + } + } + KerberosClientState::BeforeTgsExchange { + as_rep, + mech_id, + tgt_ticket, + } => { + debug!("AS exchange finished successfully."); + + client.realm = Some(as_rep.0.crealm.0.to_string()); + + let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?; + + let encryption_type = CipherSuite::try_from(usize::from(encryption_type))?; + + client.encryption_params.encryption_type = Some(encryption_type); + + let credentials = + builder.credentials_handle.as_ref().unwrap().as_ref().ok_or_else(|| { + Error::new(ErrorKind::WrongCredentialHandle, "No credentials provided") + })?; + let password = credentials.extract_password(); + + let mut session_key_extractor = match credentials { + CredentialsBuffers::AuthIdentity(_) => AsRepSessionKeyExtractor::AuthIdentity { + salt: &salt, + password: password.as_ref(), + enc_params: &mut client.encryption_params, + }, + CredentialsBuffers::Keytab(keytab) => AsRepSessionKeyExtractor::Keytab { + key: keytab.key.as_ref(), + enc_params: &client.encryption_params, + }, + #[cfg(feature = "scard")] + CredentialsBuffers::SmartCard(_) => AsRepSessionKeyExtractor::SmartCard { + dh_parameters: client.dh_parameters.as_mut().unwrap(), + enc_params: &mut client.encryption_params, + }, + }; + let session_key = session_key_extractor.session_key(&as_rep)?; + + let mut context_requirements = builder.context_requirements; + + if client.krb5_user_to_user + && !context_requirements.contains(ClientRequestFlags::USE_SESSION_KEY) + { + warn!( + "KRB5 U2U has been negotiated (selected by the server) but the USE_SESSION_KEY flag is not set. Forcibly turning it on..." + ); + context_requirements.set(ClientRequestFlags::USE_SESSION_KEY, true); + } + + client.state = KerberosState::Client(Box::new(KerberosClientState::TgsExchange { + tgs_exchange: TgsExchange::new( + as_rep.0.crealm.to_string(), + as_rep.0.ticket.0.clone(), + session_key, + // KDC-REP that the AP_REQ authenticator (cname/crealm) for the next hop is built from. + as_rep.0.clone(), + // Only meaningful for U2U; carried on the first hop and dropped afterwards. + tgt_ticket.map(|ticket| vec![ticket]), + context_requirements, + ), + context_requirements, + mech_id, + })); + + None + } + KerberosClientState::TgsExchange { + mut tgs_exchange, + context_requirements, + mech_id, + } => { + let service_principal = builder.target_name.ok_or_else(|| { + Error::new( + ErrorKind::NoCredentials, + "Service target name (service principal name) is not provided", + ) + })?; + + match client.config.kdc_resolution { + KdcResolution::IAKerb => { + let input = builder.input.as_ref().ok_or_else(|| { + Error::new(ErrorKind::InvalidToken, "input buffers must be specified") + })?; + let input_token = SecurityBuffer::find_buffer(input, BufferType::Token) + .map(|security_buffer| security_buffer.buffer.as_slice()) + .unwrap_or_default(); + + match tgs_exchange.step(client, service_principal, input_token)? { + TgsExchangeOutput::SendRequest((tgs_req, _realm)) => { + let output_token = + SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer = + generate_iakerb_proxy_message(client.iakerb_cookie.take(), tgs_req)?; + client.iakerb_gss_transcript.extend_from_slice(&output_token.buffer); + + client.state = + KerberosState::Client(Box::new(KerberosClientState::TgsExchange { + tgs_exchange, + context_requirements, + mech_id, + })); + + Some(SecurityStatus::ContinueNeeded) + } + TgsExchangeOutput::Done((tgs_rep, session_key)) => { + client.state = + KerberosState::Client(Box::new(KerberosClientState::BeforeApExchange { + tgs_rep, + session_key, + context_requirements, + mech_id, + })); + + None + } + } + } + KdcResolution::KdcUrl(_) => { + let mut response = Vec::new(); + + loop { + match tgs_exchange.step(client, service_principal, &response)? { + TgsExchangeOutput::SendRequest((tgs_req, realm)) => { + response = client + .send_for_realm(yield_point, realm, &serialize_message(&tgs_req)?) + .await?; + } + TgsExchangeOutput::Done((tgs_rep, session_key)) => { + client.state = KerberosState::Client(Box::new( + KerberosClientState::BeforeApExchange { + tgs_rep, + session_key, + context_requirements, + mech_id, + }, + )); + break; + } + } + } + + None + } + } + } + KerberosClientState::BeforeApExchange { + tgs_rep, + session_key, + context_requirements, + mech_id, + } => { + client.encryption_params.session_key = Some(session_key); + + let mut rand = StdRng::try_from_rng(&mut SysRng)?; + + let enc_type = client + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); + let authenticator_sub_key = generate_random_symmetric_key(enc_type, &mut rand); + + // the original flag is + // GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG | GSS_C_SEQUENCE_FLAG | GSS_C_CONF_FLAG | GSS_C_INTEG_FLAG + // we want to be able to turn of sign and seal, so we leave confidentiality and integrity flags out + let mut flags: GssFlags = builder.context_requirements.into(); + if flags.contains(GssFlags::GSS_C_DELEG_FLAG) { + // Below are reasons why we turn off the GSS_C_DELEG_FLAG flag. + // + // RFC4121: The Kerberos Version 5 GSS-API. Section 4.1.1: Authenticator Checksum + // https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1 + // + // "The length of the checksum field MUST be at least 24 octets when GSS_C_DELEG_FLAG is not set, + // and at least 28 octets plus Dlgth octets when GSS_C_DELEG_FLAG is set." + // Out implementation _always_ uses the 24 octets checksum and do not support Kerberos credentials delegation. + // + // "When delegation is used, a ticket-granting ticket will be transferred in a KRB_CRED message." + // We do not support KRB_CRED messages. So, the GSS_C_DELEG_FLAG flags should be turned off. + warn!( + "Kerberos ApReq Authenticator checksum GSS_C_DELEG_FLAG is not supported. Turning it off..." + ); + flags.remove(GssFlags::GSS_C_DELEG_FLAG); + } + debug!(?flags, "ApReq Authenticator checksum flags"); + + let mut checksum_value = ChecksumValues::default(); + checksum_value.set_flags(flags); + + let extensions = match client.config.kdc_resolution { + KdcResolution::IAKerb => { + // IAKerb requires an extension of type `GSS_EXTS_FINISHED` that contains `KRB-FINISHED` + // with checksum of all IAKerb GSS-API tokens, concatenated in the chronological order. + // + // [IAKERB Finish Message](https://datatracker.ietf.org/doc/html/draft-ietf-kitten-iakerb-03#section-4) + vec![generate_authenticator_extension( + &authenticator_sub_key, + &client.iakerb_gss_transcript, + &enc_type.cipher().checksum_type(), + )?] + } + KdcResolution::KdcUrl(_) => Vec::new(), + }; + + let authenticator_options = GenerateAuthenticatorOptions { + kdc_rep: &tgs_rep.0, + // The AP_REQ Authenticator sequence number should be the same as `seq_num` in the first Kerberos Wrap/MIC token generated + // by the `encrypt_message`/`generate_mic_token` method. So, we set the next sequence number but do not increment the counter, + // which will be incremented on each `encrypt_message`/`generate_mic_token` method call. + seq_num: Some(client.seq_number + 1), + sub_key: Some(EncKey { + key_type: enc_type.clone(), + key_value: authenticator_sub_key, + }), + + checksum: Some(ChecksumOptions { + checksum_type: AUTHENTICATOR_CHECKSUM_TYPE.to_vec(), + checksum_value, + }), + channel_bindings: client.channel_bindings.as_ref(), + extensions, + }; + + let now = client.current_kdc_time()?; + let authenticator = generate_authenticator_at(authenticator_options, now)?; + + let ap_req = generate_ap_req( + tgs_rep.0.ticket.0, + client + .encryption_params + .session_key + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?, + &authenticator, + &client.encryption_params, + context_requirements.into(), + )?; + + let encoded_neg_ap_req = + if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { + generate_krb_message(mech_id, AP_REQ_TOKEN_ID, ap_req)? + } else { + // Do not wrap if the `USE_DCE_STYLE` flag is set. + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 + picky_asn1_der::to_vec(&ap_req)? + }; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer = encoded_neg_ap_req; + + client.state = KerberosState::Client(Box::new(KerberosClientState::ApExchange)); + + Some(SecurityStatus::ContinueNeeded) + } + KerberosClientState::ApExchange => { + let input = builder + .input + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified"))?; + let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; + + if builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { + // The `EC` field depends on the authentication type. For example, during RDP auth + // it is equal to 0, but during RPC auth it is equal to EC. + client.encryption_params.ec = EC; + + use picky_krb::messages::ApRep; + + let ap_rep: ApRep = picky_asn1_der::from_bytes(&input_token.buffer)?; + + let session_key = client + .encryption_params + .session_key + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; + let ap_rep_enc_part = decrypt_ap_rep(&ap_rep, session_key, &client.encryption_params)?; + let sub_session_key = extract_sub_session_key_from_ap_rep(&ap_rep_enc_part)?; + client.remote_seq_number = extract_seq_number_from_ap_rep(&ap_rep_enc_part)?; + + let seq_number_bytes = ap_rep_enc_part + .0 + .seq_number + .0 + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing seq-number in ap_rep"))? + .0 + .0 + .clone(); + + trace!(?sub_session_key, "DCE AP_REP sub-session key"); + + client.encryption_params.sub_session_key = Some(sub_session_key); + + let ap_rep = generate_ap_rep(session_key, seq_number_bytes, &client.encryption_params)?; + let ap_rep = picky_asn1_der::to_vec(&ap_rep)?; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer.write_all(&ap_rep)?; + } else { + let ap_rep = decode_krb_message::(&input_token.buffer, AP_REP_TOKEN_ID)?; + + let session_key = client + .encryption_params + .session_key + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; + let ap_rep_enc_part = decrypt_ap_rep(&ap_rep, session_key, &client.encryption_params)?; + let sub_session_key = extract_sub_session_key_from_ap_rep(&ap_rep_enc_part)?; + client.remote_seq_number = extract_seq_number_from_ap_rep(&ap_rep_enc_part)?; + + client.encryption_params.sub_session_key = Some(sub_session_key); + } + + client.state = KerberosState::Final; + Some(SecurityStatus::Ok) + } + } + } + KerberosState::Failed => { + return Err(Error::new( + ErrorKind::InternalError, + "Kerberos client is in failed state", + )); + } + wrong_state @ (KerberosState::Final | KerberosState::Server(_)) => { + return Err(Error::new( + ErrorKind::OutOfSequence, + format!("got wrong Kerberos state: {:?}", wrong_state), + )); + } + }; + if let Some(status) = status { trace!(output_buffers = ?builder.output); return Ok(InitializeSecurityContextResult { - status: SecurityStatus::ContinueNeeded, + status, flags: ClientResponseFlags::empty(), expiry: None, }); - } else { - client.state = KerberosState::Preauthentication; } } - - let status = match client.state { - KerberosState::Preauthentication => { - let input = builder - .input - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?; - - if let Ok(sec_buffer) = - SecurityBuffer::find_buffer(builder.input.as_ref().unwrap(), BufferType::ChannelBindings) - { - client.channel_bindings = Some(ChannelBindings::from_bytes(&sec_buffer.buffer)?); - } - - let input_token = SecurityBuffer::find_buffer(input, BufferType::Token) - .map(|security_buffer| security_buffer.buffer.as_slice()) - .unwrap_or_default(); - - let (tgt_ticket, mech_id) = if let Some((tbt_ticket, mech_oid)) = extract_tgt_ticket_with_oid(input_token)? - { - (Some(tbt_ticket), mech_oid.0) - } else { - (None, oids::krb5()) - }; - client.krb5_user_to_user = mech_id == oids::krb5_user_to_user(); - - let credentials = builder - .credentials_handle - .as_ref() - .unwrap() - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::WrongCredentialHandle, "No credentials provided"))?; - - let (username, password, realm, cname_type) = match credentials { - CredentialsBuffers::AuthIdentity(auth_identity) => { - let username = auth_identity.user.to_string(); - let domain = auth_identity.domain.to_string(); - let password = auth_identity.password.as_ref().as_ref().to_string(); - - let realm = get_client_principal_realm(&username, &domain); - let cname_type = get_client_principal_name_type(&username, &domain); - - (username, password, realm, cname_type) - } - #[cfg(feature = "scard")] - CredentialsBuffers::SmartCard(smart_card) => { - let username = smart_card.username.to_string(); - let password = smart_card.pin.as_ref().as_ref().to_string(); - - let realm = get_client_principal_realm(&username, ""); - let cname_type = get_client_principal_name_type(&username, ""); - - (username, password, realm.to_uppercase(), cname_type) - } - CredentialsBuffers::Keytab(keytab) => { - // The name type is read off the principal's user name format explicitly. - let ClientPrincipalName { - name, - realm_domain, - name_type, - } = get_client_principal_name(&keytab.principal); - - let realm = get_client_principal_realm(name, realm_domain); - - // No password: the keytab key is used directly for pre-auth. - (name.to_owned(), String::new(), realm, name_type) - } - }; - client.realm = Some(realm.clone()); - - let mut rand = StdRng::try_from_rng(&mut SysRng)?; - let options = GenerateAsReqOptions { - realm: &realm, - username: &username, - cname_type, - snames: &[TGT_SERVICE_NAME, &realm], - nonce: generate_nonce(&mut rand), - hostname: &client.config.client_computer_name, - context_requirements: builder.context_requirements, - }; - let kdc_req_body = generate_as_req_kdc_body(&options)?; - - let pa_data_options = match credentials { - CredentialsBuffers::AuthIdentity(auth_identity) => { - let domain = auth_identity.domain.to_string(); - let salt = format!("{domain}{username}").into_bytes(); - - AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions { - password: &password, - salt, - enc_params: client.encryption_params.clone(), - with_pre_auth: false, - }) - } - CredentialsBuffers::Keytab(keytab) => AsReqPaDataOptions::Keytab(GenerateKeytabPaDataOptions { - key: keytab.key.clone(), - key_enctype: keytab.key_enctype.clone(), - with_pre_auth: false, - }), - #[cfg(feature = "scard")] - CredentialsBuffers::SmartCard(scard_identity_buffer) => { - use sha1::{Digest, Sha1}; - - use crate::pku2u::generate_client_dh_parameters; - use crate::smartcard::SmartCard; - use crate::{SmartCardIdentity, pk_init}; - - let scard_identity = SmartCardIdentity::try_from(scard_identity_buffer)?; - - let mut smart_card = SmartCard::from_credentials(&scard_identity)?; - let p2p_cert = scard_identity.certificate; - - client.dh_parameters = Some(generate_client_dh_parameters(&mut rand)); - - AsReqPaDataOptions::SmartCard(Box::new(pk_init::GenerateAsPaDataOptions { - p2p_cert, - kdc_req_body: &kdc_req_body, - dh_parameters: client.dh_parameters.clone().unwrap(), - sign_data: Box::new(move |data_to_sign| { - let mut sha1 = Sha1::new(); - sha1.update(data_to_sign); - let digest = sha1.finalize().to_vec(); - - smart_card.sign(digest) - }), - with_pre_auth: false, - authenticator_nonce: generate_nonce(&mut rand), - })) - } - }; - - let as_rep = as_exchange(client, yield_point, &kdc_req_body, pa_data_options).await?; - - debug!("AS exchange finished successfully."); - - client.realm = Some(as_rep.0.crealm.0.to_string()); - - let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?; - - let encryption_type = CipherSuite::try_from(usize::from(encryption_type))?; - - client.encryption_params.encryption_type = Some(encryption_type); - - let mut session_key_extractor = match credentials { - CredentialsBuffers::AuthIdentity(_) => AsRepSessionKeyExtractor::AuthIdentity { - salt: &salt, - password: &password, - enc_params: &mut client.encryption_params, - }, - CredentialsBuffers::Keytab(keytab) => AsRepSessionKeyExtractor::Keytab { - key: keytab.key.as_ref(), - enc_params: &client.encryption_params, - }, - #[cfg(feature = "scard")] - CredentialsBuffers::SmartCard(_) => AsRepSessionKeyExtractor::SmartCard { - dh_parameters: client.dh_parameters.as_mut().unwrap(), - enc_params: &mut client.encryption_params, - }, - }; - let session_key_1 = session_key_extractor.session_key(&as_rep)?; - - let service_principal = builder.target_name.ok_or_else(|| { - Error::new( - ErrorKind::NoCredentials, - "Service target name (service principal name) is not provided", - ) - })?; - - let mut context_requirements = builder.context_requirements; - - if client.krb5_user_to_user && !context_requirements.contains(ClientRequestFlags::USE_SESSION_KEY) { - warn!( - "KRB5 U2U has been negotiated (selected by the server) but the USE_SESSION_KEY flag is not set. Forcibly turning it on..." - ); - context_requirements.set(ClientRequestFlags::USE_SESSION_KEY, true); - } - - // Cross-realm referral chasing (RFC 4120 §3.3.3.2 / MS-KILE). - // - // * [Cross-Realm Operation](https://www.rfc-editor.org/rfc/rfc4120.html#section-1.2) - // * [Server Referrals](https://www.rfc-editor.org/rfc/rfc6806.html#section-8) - // - // A KDC can only issue tickets for principals in its own realm. When the requested - // service lives in another realm (e.g. a user in `RJM.LOCAL` targeting a host in the - // child realm `DEV.RJM.LOCAL`), the KDC does not return the service ticket. Instead it - // returns a referral TGT whose `sname` is `krbtgt/`, and the client must - // re-issue the TGS-REQ for the same service to ``'s KDC using that referral - // TGT. We loop until the returned ticket's `sname` matches the requested service (i.e. - // it is no longer a `krbtgt/...` referral). - // - // The referral hop is routed via `send_for_realm`, which resolves the target realm's - // KDC through `SSPI_KDC_URL_` (env) / krb5.conf / DNS SRV rather than the pinned - // home-realm KDC, which cannot decrypt a `krbtgt/` referral ticket. - const MAX_REFERRAL_HOPS: usize = 10; - - let mut realm = as_rep.0.crealm.0.to_string(); - let mut ticket = as_rep.0.ticket.0.clone(); - let mut tgt_session_key = session_key_1; - // KDC-REP that the AP_REQ authenticator (cname/crealm) for the next hop is built from. - let mut auth_rep = as_rep.0.clone(); - // Only meaningful for U2U; carried on the first hop and dropped afterwards. - let mut additional_tickets = tgt_ticket.map(|ticket| vec![ticket]); - let mut hops = 0; - - let (tgs_rep, session_key_2) = loop { - let now = client.current_kdc_time()?; - let mut authenticator = generate_authenticator_at( - GenerateAuthenticatorOptions { - kdc_rep: &auth_rep, - seq_num: Some(rand.next_u32()), - sub_key: None, - checksum: None, - channel_bindings: client.channel_bindings.as_ref(), - extensions: Vec::new(), - }, - now, - )?; - - let tgs_req = generate_tgs_req(GenerateTgsReqOptions { - realm: &realm, - service_principal, - session_key: &tgt_session_key, - ticket, - authenticator: &mut authenticator, - additional_tickets: additional_tickets.take(), - enc_params: &client.encryption_params, - context_requirements, - })?; - - let response = client - .send_for_realm(yield_point, &realm, &serialize_message(&tgs_req)?) - .await?; - - let Some(response) = response.get(4..) else { - return Err(Error::new( - ErrorKind::InternalError, - "the KDC reply message is too small: expected at least 4 bytes", - )); - }; - - // first 4 bytes are message len. skipping them - let mut d = picky_asn1_der::Deserializer::new_from_bytes(response); - let tgs_rep: KrbResult = KrbResult::deserialize(&mut d)?; - let tgs_rep = tgs_rep?; - - let session_key = - extract_session_key_from_tgs_rep(&tgs_rep, &tgt_session_key, &client.encryption_params)?; - - // A referral TGT is identified by an `sname` of the form `krbtgt/`. - let Some(next_realm) = referral_target_realm(&tgs_rep.0.ticket.0.0.sname.0) else { - debug!("TGS exchange finished successfully"); - break (tgs_rep, session_key); - }; - debug!(%realm, %next_realm, "Received cross-realm referral TGT; chasing referral"); - - hops += 1; - if hops >= MAX_REFERRAL_HOPS { - return Err(Error::new( - ErrorKind::NoAuthenticatingAuthority, - format!( - "exceeded maximum Kerberos referral hops ({MAX_REFERRAL_HOPS}) resolving {service_principal}" - ), - )); - } - if next_realm.eq_ignore_ascii_case(&realm) { - return Err(Error::new( - ErrorKind::NoAuthenticatingAuthority, - format!("Kerberos referral did not progress past realm `{realm}`"), - )); - } - - ticket = tgs_rep.0.ticket.0.clone(); - tgt_session_key = session_key; - auth_rep = tgs_rep.0; - realm = next_realm; - }; - - client.encryption_params.session_key = Some(session_key_2); - - let enc_type = client - .encryption_params - .encryption_type - .as_ref() - .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); - let authenticator_sub_key = generate_random_symmetric_key(enc_type, &mut rand); - - // the original flag is - // GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG | GSS_C_SEQUENCE_FLAG | GSS_C_CONF_FLAG | GSS_C_INTEG_FLAG - // we want to be able to turn of sign and seal, so we leave confidentiality and integrity flags out - let mut flags: GssFlags = builder.context_requirements.into(); - if flags.contains(GssFlags::GSS_C_DELEG_FLAG) { - // Below are reasons why we turn off the GSS_C_DELEG_FLAG flag. - // - // RFC4121: The Kerberos Version 5 GSS-API. Section 4.1.1: Authenticator Checksum - // https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1 - // - // "The length of the checksum field MUST be at least 24 octets when GSS_C_DELEG_FLAG is not set, - // and at least 28 octets plus Dlgth octets when GSS_C_DELEG_FLAG is set." - // Out implementation _always_ uses the 24 octets checksum and do not support Kerberos credentials delegation. - // - // "When delegation is used, a ticket-granting ticket will be transferred in a KRB_CRED message." - // We do not support KRB_CRED messages. So, the GSS_C_DELEG_FLAG flags should be turned off. - warn!("Kerberos ApReq Authenticator checksum GSS_C_DELEG_FLAG is not supported. Turning it off..."); - flags.remove(GssFlags::GSS_C_DELEG_FLAG); - } - debug!(?flags, "ApReq Authenticator checksum flags"); - - let mut checksum_value = ChecksumValues::default(); - checksum_value.set_flags(flags); - - let authenticator_options = GenerateAuthenticatorOptions { - kdc_rep: &tgs_rep.0, - // The AP_REQ Authenticator sequence number should be the same as `seq_num` in the first Kerberos Wrap/MIC token generated - // by the `encrypt_message`/`generate_mic_token` method. So, we set the next sequence number but do not increment the counter, - // which will be incremented on each `encrypt_message`/`generate_mic_token` method call. - seq_num: Some(client.seq_number + 1), - sub_key: Some(EncKey { - key_type: enc_type.clone(), - key_value: authenticator_sub_key, - }), - - checksum: Some(ChecksumOptions { - checksum_type: AUTHENTICATOR_CHECKSUM_TYPE.to_vec(), - checksum_value, - }), - channel_bindings: client.channel_bindings.as_ref(), - extensions: Vec::new(), - }; - - let now = client.current_kdc_time()?; - let authenticator = generate_authenticator_at(authenticator_options, now)?; - - let ap_req = generate_ap_req( - tgs_rep.0.ticket.0, - client - .encryption_params - .session_key - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?, - &authenticator, - &client.encryption_params, - context_requirements.into(), - )?; - - let encoded_neg_ap_req = if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { - generate_krb_message(mech_id, AP_REQ_TOKEN_ID, ap_req)? - } else { - // Do not wrap if the `USE_DCE_STYLE` flag is set. - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 - picky_asn1_der::to_vec(&ap_req)? - }; - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - output_token.buffer = encoded_neg_ap_req; - - client.state = KerberosState::ApExchange; - - SecurityStatus::ContinueNeeded - } - KerberosState::ApExchange => { - let input = builder - .input - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified"))?; - let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; - - if builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { - // The `EC` field depends on the authentication type. For example, during RDP auth - // it is equal to 0, but during RPC auth it is equal to EC. - client.encryption_params.ec = EC; - - use picky_krb::messages::ApRep; - - let ap_rep: ApRep = picky_asn1_der::from_bytes(&input_token.buffer)?; - - let session_key = client - .encryption_params - .session_key - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; - let ap_rep_enc_part = decrypt_ap_rep(&ap_rep, session_key, &client.encryption_params)?; - let sub_session_key = extract_sub_session_key_from_ap_rep(&ap_rep_enc_part)?; - client.remote_seq_number = extract_seq_number_from_ap_rep(&ap_rep_enc_part)?; - - let seq_number_bytes = ap_rep_enc_part - .0 - .seq_number - .0 - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing seq-number in ap_rep"))? - .0 - .0 - .clone(); - - trace!(?sub_session_key, "DCE AP_REP sub-session key"); - - client.encryption_params.sub_session_key = Some(sub_session_key); - - let ap_rep = generate_ap_rep(session_key, seq_number_bytes, &client.encryption_params)?; - let ap_rep = picky_asn1_der::to_vec(&ap_rep)?; - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - output_token.buffer.write_all(&ap_rep)?; - } else { - let ap_rep = decode_krb_message::(&input_token.buffer, AP_REP_TOKEN_ID)?; - - let session_key = client - .encryption_params - .session_key - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; - let ap_rep_enc_part = decrypt_ap_rep(&ap_rep, session_key, &client.encryption_params)?; - let sub_session_key = extract_sub_session_key_from_ap_rep(&ap_rep_enc_part)?; - client.remote_seq_number = extract_seq_number_from_ap_rep(&ap_rep_enc_part)?; - - client.encryption_params.sub_session_key = Some(sub_session_key); - } - - client.state = KerberosState::Final; - SecurityStatus::Ok - } - KerberosState::Final | KerberosState::TgtExchange => { - return Err(Error::new( - ErrorKind::OutOfSequence, - format!("got wrong Kerberos state: {:?}", client.state), - )); - } - }; - - trace!(output_buffers = ?builder.output); - - Ok(InitializeSecurityContextResult { - status, - flags: ClientResponseFlags::empty(), - expiry: None, - }) } #[cfg(test)] diff --git a/src/kerberos/config.rs b/src/kerberos/config.rs index c93542c8..9f5974b7 100644 --- a/src/kerberos/config.rs +++ b/src/kerberos/config.rs @@ -8,9 +8,11 @@ use crate::kerberos::ServerProperties; use crate::negotiate::{NegotiatedProtocol, ProtocolConfig}; use crate::{Kerberos, Result}; -/// Kerberos client configuration. +/// Strategy for resolving the KDC to use for Kerberos authentication. #[derive(Clone, Debug)] -pub struct KerberosConfig { +pub enum KdcResolution { + /// Use IAKerb extension to proxy KDC communication through the server to the LocalKDC. + IAKerb, /// KDC URL /// /// Depending on the scheme it is expected to be either: @@ -23,7 +25,14 @@ pub struct KerberosConfig { /// in order to communicate with the KDC server directly. /// /// [KKDCP]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kkdcp/5bcebb8d-b747-4ee5-9453-428aec1c5c38 - pub kdc_url: Option, + KdcUrl(Option), +} + +/// Kerberos client configuration. +#[derive(Clone, Debug)] +pub struct KerberosConfig { + /// Strategy for resolving the KDC to use for Kerberos authentication. + pub kdc_resolution: KdcResolution, /// Computer name, or "workstation name", of the client machine performing the authentication attempt /// /// This is also referred to as the "Source Workstation", i.e.: the name of the computer attempting to logon. @@ -51,20 +60,32 @@ pub fn parse_kdc_url(kdc_url: &str) -> Option { } impl KerberosConfig { - pub fn new(kdc_url: &str, client_computer_name: String) -> Self { + pub fn new_with_kdc_url(kdc_url: &str, client_computer_name: String) -> Self { let kdc_url = parse_kdc_url(kdc_url); Self { - kdc_url, + kdc_resolution: KdcResolution::KdcUrl(kdc_url), + client_computer_name, + } + } + + pub fn new_with_iakerb(client_computer_name: String) -> Self { + Self { + kdc_resolution: KdcResolution::IAKerb, client_computer_name, } } pub fn get_kdc_url(self, domain: &str) -> Option { - if let Some(kdc_url) = self.kdc_url { - Some(kdc_url) - } else { - detect_kdc_url(domain) + match self.kdc_resolution { + KdcResolution::IAKerb => None, + KdcResolution::KdcUrl(kdc_url) => { + if let Some(kdc_url) = kdc_url { + Some(kdc_url) + } else { + detect_kdc_url(domain) + } + } } } } diff --git a/src/kerberos/messages.rs b/src/kerberos/messages.rs index bc391c70..83328e4b 100644 --- a/src/kerberos/messages.rs +++ b/src/kerberos/messages.rs @@ -3,8 +3,9 @@ use std::fmt::Debug; use oid::ObjectIdentifier; -use picky_asn1::wrapper::ObjectIdentifierAsn1; -use picky_krb::gss_api::{ApplicationTag0, KrbMessage}; +use picky_asn1::wrapper::{ExplicitContextTag1, ObjectIdentifierAsn1, Optional}; +use picky_krb::gss_api::{ApplicationTag0, IAKerbProxyMessage, KrbMessage}; +use picky_krb::messages::{IAKerbCookie, IAKerbHeader}; use serde::Serialize; use serde::de::DeserializeOwned; @@ -40,3 +41,27 @@ pub(super) fn generate_krb_message( Ok(picky_asn1_der::to_vec(&krb_blob)?) } + +pub(super) fn generate_iakerb_proxy_message( + cookie: IAKerbCookie, + krb_msg: T, +) -> Result> { + let iakerb_proxy_blob = ApplicationTag0(IAKerbProxyMessage { + header: IAKerbHeader { + // Although the IAKerb RFC specifies that the `target_realm` must be provided, + // the Windows implementation does not use it. + // The specification also states that the `target_realm` can be retrieved by sending + // the `IAKerbProxyMessage` with empty `target_realm` and no Kerberos message. However, + // the Windows implementation returns an error if the Kerberos message is absent. + // Therefore, we leave the `target_realm` empty. + // + // [IAKERB Realm Retrieval](https://datatracker.ietf.org/doc/html/draft-ietf-kitten-iakerb-03#section-3.1) + target_realm: ExplicitContextTag1::from(String::new()), + cookie: Optional(cookie), + flags: Optional(None), + }, + krb_msg, + }); + + Ok(picky_asn1_der::to_vec(&iakerb_proxy_blob)?) +} diff --git a/src/kerberos/mod.rs b/src/kerberos/mod.rs index fdc7c729..d6191082 100644 --- a/src/kerberos/mod.rs +++ b/src/kerberos/mod.rs @@ -9,7 +9,6 @@ pub mod server; mod tests; pub(crate) mod utils; -use std::fmt::Debug; use std::sync::LazyLock; use picky_asn1::restricted_string::IA5String; @@ -18,14 +17,14 @@ use picky_krb::crypto::aes::{AesSize, checksum_sha_aes}; use picky_krb::crypto::{CipherSuite, DecryptWithoutChecksum, EncryptWithoutChecksum}; use picky_krb::data_types::KerberosStringAsn1; use picky_krb::gss_api::WrapToken; -use picky_krb::messages::KdcProxyMessage; +use picky_krb::messages::{IAKerbCookie, KdcProxyMessage}; use rand::rngs::{StdRng, SysRng}; use rand_core::{Rng as _, SeedableRng as _}; use time::{Duration, OffsetDateTime}; use url::Url; pub use self::client::initialize_security_context; -use self::config::KerberosConfig; +use self::config::{KdcResolution, KerberosConfig}; pub use self::encryption_params::EncryptionParams; pub use self::server::{ServerProperties, accept_security_context}; use super::channel_bindings::ChannelBindings; @@ -34,6 +33,8 @@ use crate::generator::{ GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, NetworkRequest, YieldPointLocal, }; +use crate::kerberos::client::KerberosClientState; +use crate::kerberos::server::KerberosServerState; use crate::network_client::NetworkProtocol; #[cfg(feature = "scard")] use crate::pk_init::DhParameters; @@ -78,12 +79,13 @@ pub static PACKAGE_INFO: LazyLock = LazyLock::new(|| PackageInfo { comment: String::from("Kerberos Security Package"), }); -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub enum KerberosState { - TgtExchange, - Preauthentication, - ApExchange, + Client(Box), + Server(KerberosServerState), Final, + #[default] + Failed, } #[derive(Debug, Clone)] @@ -94,7 +96,6 @@ pub struct Kerberos { pub(crate) encryption_params: EncryptionParams, pub(crate) seq_number: u32, pub(crate) realm: Option, - pub(crate) kdc_url: Option, pub(crate) channel_bindings: Option, #[cfg(feature = "scard")] pub(crate) dh_parameters: Option, @@ -103,21 +104,23 @@ pub struct Kerberos { pub(crate) remote_seq_number: u32, /// KDC time minus local time, learned from a clock-skew error during AS pre-authentication. pub(crate) clock_offset: Duration, + /// Opaque data, if sent by the server, must be copied verbatim into the next [`IAKerbProxyMessage`](picky_krb::gss_api::IAKerbProxyMessage). + pub(crate) iakerb_cookie: IAKerbCookie, + /// Transcript of the `IAKerb` `GSS-API` tokens exchanged between the client and the server. + pub(crate) iakerb_gss_transcript: Vec, } impl Kerberos { pub fn new_client_from_config(config: KerberosConfig) -> Result { - let kdc_url = config.kdc_url.clone(); let mut rand = StdRng::try_from_rng(&mut SysRng)?; Ok(Self { - state: KerberosState::TgtExchange, + state: KerberosState::Client(Box::default()), config, auth_identity: None, encryption_params: EncryptionParams::default_for_client(), seq_number: rand.next_u32(), realm: None, - kdc_url, channel_bindings: None, #[cfg(feature = "scard")] dh_parameters: None, @@ -125,21 +128,21 @@ impl Kerberos { server: None, remote_seq_number: 0, clock_offset: Duration::ZERO, + iakerb_cookie: None, + iakerb_gss_transcript: Vec::new(), }) } pub fn new_server_from_config(config: KerberosConfig, server_properties: ServerProperties) -> Result { - let kdc_url = config.kdc_url.clone(); let mut rand = StdRng::try_from_rng(&mut SysRng)?; Ok(Self { - state: KerberosState::TgtExchange, + state: KerberosState::Server(KerberosServerState::TgtExchange), config, auth_identity: None, encryption_params: EncryptionParams::default_for_server(), seq_number: rand.next_u32(), realm: None, - kdc_url, channel_bindings: None, #[cfg(feature = "scard")] dh_parameters: None, @@ -147,6 +150,8 @@ impl Kerberos { server: Some(Box::new(server_properties)), remote_seq_number: 0, clock_offset: Duration::ZERO, + iakerb_cookie: None, + iakerb_gss_transcript: Vec::new(), }) } @@ -154,6 +159,10 @@ impl Kerberos { self.server.is_none() } + pub fn is_iakerb(&self) -> bool { + matches!(self.config.kdc_resolution, KdcResolution::IAKerb) + } + pub(crate) fn current_kdc_time(&self) -> Result { OffsetDateTime::now_utc() .checked_add(self.clock_offset) @@ -171,12 +180,17 @@ impl Kerberos { #[instrument(level = "debug", ret, skip(self))] pub fn get_kdc(&self) -> Option<(String, Url)> { - let realm = self.realm.to_owned()?; - if let Some(kdc_url) = &self.kdc_url { - Some((realm, kdc_url.to_owned())) - } else { - let kdc_url = detect_kdc_url(&realm)?; - Some((realm, kdc_url)) + match &self.config.kdc_resolution { + KdcResolution::IAKerb => None, + KdcResolution::KdcUrl(kdc_url) => { + let realm = self.realm.to_owned()?; + if let Some(kdc_url) = kdc_url { + Some((realm, kdc_url.to_owned())) + } else { + let kdc_url = detect_kdc_url(&realm)?; + Some((realm, kdc_url)) + } + } } } @@ -196,7 +210,10 @@ impl Kerberos { /// chase a referral into a child/trusted realm without changing the pinned home-realm KDC. async fn send_for_realm(&self, yield_point: &mut YieldPointLocal, realm: &str, data: &[u8]) -> Result> { let kdc_url = if self.realm.as_deref() == Some(realm) { - self.kdc_url.clone().or_else(|| detect_kdc_url(realm)) + match &self.config.kdc_resolution { + KdcResolution::KdcUrl(kdc_url) => kdc_url.clone().or_else(|| detect_kdc_url(realm)), + KdcResolution::IAKerb => None, + } } else { detect_kdc_url(realm) } @@ -738,7 +755,7 @@ impl<'a> Kerberos { } pub(crate) async fn accept_security_context_impl( - &'a mut self, + &mut self, yield_point: &mut YieldPointLocal, builder: crate::builders::FilledAcceptSecurityContext<'a, ::CredentialsHandle>, ) -> Result { @@ -818,6 +835,7 @@ pub mod test_data { use super::{EncryptionParams, KerberosConfig, KerberosState}; use crate::kerberos::ServerProperties; + use crate::kerberos::config::KdcResolution; use crate::{AuthIdentityBuffers, CredentialsBuffers, Kerberos, Secret, Utf16String, ZeroizedUtf16String}; const SESSION_KEY: &[u8] = &[ @@ -833,7 +851,7 @@ pub mod test_data { Kerberos { state: KerberosState::Final, config: KerberosConfig { - kdc_url: None, + kdc_resolution: KdcResolution::KdcUrl(None), client_computer_name: "hostname".into(), }, auth_identity: Some(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers { @@ -851,7 +869,6 @@ pub mod test_data { }, seq_number: 1234, realm: None, - kdc_url: None, channel_bindings: None, #[cfg(feature = "scard")] dh_parameters: None, @@ -859,6 +876,8 @@ pub mod test_data { server: None, remote_seq_number: 0, clock_offset: time::Duration::ZERO, + iakerb_cookie: None, + iakerb_gss_transcript: Vec::new(), } } @@ -885,7 +904,7 @@ pub mod test_data { Kerberos { state: KerberosState::Final, config: KerberosConfig { - kdc_url: None, + kdc_resolution: KdcResolution::KdcUrl(None), client_computer_name: "hostname".into(), }, auth_identity: None, @@ -899,7 +918,6 @@ pub mod test_data { }, seq_number: 0, realm: None, - kdc_url: None, channel_bindings: None, #[cfg(feature = "scard")] dh_parameters: None, @@ -907,6 +925,8 @@ pub mod test_data { server: Some(Box::new(fake_server_properties())), remote_seq_number: 0, clock_offset: time::Duration::ZERO, + iakerb_cookie: None, + iakerb_gss_transcript: Vec::new(), } } } diff --git a/src/kerberos/pa_datas.rs b/src/kerberos/pa_datas.rs index ba82d800..d02d4062 100644 --- a/src/kerberos/pa_datas.rs +++ b/src/kerberos/pa_datas.rs @@ -129,7 +129,7 @@ impl AsRepSessionKeyExtractor<'_> { let key = generate_key( check_if_empty!(dh_parameters.other_public_key.as_ref(), "dh public key is not set"), - &dh_parameters.private_key, + dh_parameters.private_key.as_ref(), &dh_parameters.modulus, Some(DhNonce { client_nonce: check_if_empty!(dh_parameters.client_nonce.as_ref(), "dh client none is not set"), diff --git a/src/kerberos/server/as_exchange.rs b/src/kerberos/server/as_exchange.rs index 834bbd23..7a2142fc 100644 --- a/src/kerberos/server/as_exchange.rs +++ b/src/kerberos/server/as_exchange.rs @@ -1,17 +1,21 @@ +use picky_krb::constants::error_codes::KRB_AP_ERR_SKEW; use picky_krb::crypto::CipherSuite; -use picky_krb::data_types::Ticket; -use picky_krb::messages::TgtReq; +use picky_krb::data_types::{KrbResult, ResultExt, Ticket}; +use picky_krb::messages::{AsRep, KdcReqBody, TgtReq}; use rand::rngs::{StdRng, SysRng}; use rand_core::SeedableRng as _; +use time::OffsetDateTime; use crate::generator::YieldPointLocal; -use crate::kerberos::client::extractors::extract_encryption_params_from_as_rep; +use crate::kerberos::TGT_SERVICE_NAME; +use crate::kerberos::client::extractors::{extract_encryption_params_from_as_rep, extract_salt_from_krb_error}; use crate::kerberos::client::generators::{ - GenerateAsPaDataOptions, GenerateAsReqOptions, generate_as_req_kdc_body, generate_nonce, + GenerateAsPaDataOptions, GenerateAsReqOptions, generate_as_req, generate_as_req_kdc_body, generate_nonce, }; +use crate::kerberos::client::kdc::as_exchange::clock_offset_from_error; use crate::kerberos::client::principal::{get_client_principal_name_type, get_client_principal_realm}; use crate::kerberos::pa_datas::{AsRepSessionKeyExtractor, AsReqPaDataOptions}; -use crate::kerberos::{TGT_SERVICE_NAME, client}; +use crate::kerberos::utils::serialize_message; use crate::{ClientRequestFlags, CredentialsBuffers, Error, ErrorKind, Kerberos, Result}; /// Requests the TGT ticket from KDC. @@ -110,7 +114,7 @@ pub(crate) async fn request_tgt( } }; - let as_rep = client::as_exchange(server, yield_point, &kdc_req_body, pa_data_options).await?; + let as_rep = as_exchange(server, yield_point, &kdc_req_body, pa_data_options).await?; debug!("AS exchange finished successfully."); @@ -137,3 +141,81 @@ pub(crate) async fn request_tgt( Ok(as_rep.0.ticket.0) } + +/// Performs the AS exchange as specified in [RFC 4120, section 3.1](https://www.rfc-editor.org/rfc/rfc4120#section-3.1). +/// +/// On a KDC clock-skew error, retries pre-authentication once using the server +/// time in the error. The offset is kept on the client context for subsequent +/// TGS and AP authenticators; other errors are returned without retrying. +pub(crate) async fn as_exchange( + client: &mut Kerberos, + yield_point: &mut YieldPointLocal, + kdc_req_body: &KdcReqBody, + mut pa_data_options: AsReqPaDataOptions<'_>, +) -> Result { + pa_data_options.with_pre_auth(false); + let pa_datas = pa_data_options.generate(client.current_kdc_time()?)?; + let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); + + let response = client.send(yield_point, &serialize_message(&as_req)?).await?; + + // first 4 bytes are message len. skipping them + { + if response.len() < 4 { + return Err(Error::new( + ErrorKind::InternalError, + "the KDC reply message is too small: expected at least 4 bytes", + )); + } + + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); + let as_rep: KrbResult = KrbResult::deserialize(&mut d)?; + + if as_rep.is_ok() { + error!("KDC replied with AS_REP to the AS_REQ without the encrypted timestamp. The KRB_ERROR expected."); + + return Err(Error::new( + ErrorKind::InvalidToken, + "KDC server should not process AS_REQ without the pa-pac data", + )); + } + + if let Some(correct_salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? { + debug!("salt extracted successfully from the KRB_ERROR"); + + pa_data_options.with_salt(correct_salt.into_bytes()); + } + } + + pa_data_options.with_pre_auth(true); + let mut retried_skew = false; + loop { + let pa_datas = pa_data_options.generate(client.current_kdc_time()?)?; + let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); + let response = client.send(yield_point, &serialize_message(&as_req)?).await?; + let received_at = OffsetDateTime::now_utc(); + + if response.len() < 4 { + return Err(Error::new( + ErrorKind::InternalError, + "the KDC reply message is too small: expected at least 4 bytes", + )); + } + + // first 4 bytes are message len. skipping them + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); + let as_rep: KrbResult = KrbResult::deserialize(&mut d)?; + match as_rep { + Ok(as_rep) => return Ok(as_rep), + Err(err) if !retried_skew && err.0.error_code.0 == KRB_AP_ERR_SKEW => { + client.clock_offset = clock_offset_from_error(&err, received_at)?; + retried_skew = true; + debug!(offset = ?client.clock_offset, "Retrying AS exchange with KDC clock offset"); + } + Err(err) => { + error!(?err, "AS exchange error"); + return Err(err.into()); + } + } + } +} diff --git a/src/kerberos/server/mod.rs b/src/kerberos/server/mod.rs index f177c948..3f6d92d8 100644 --- a/src/kerberos/server/mod.rs +++ b/src/kerberos/server/mod.rs @@ -35,6 +35,13 @@ use crate::{ Secret, SecurityBuffer, SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, Username, }; +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum KerberosServerState { + TgtExchange, + Preauthentication, + ApExchange, +} + /// Additional properties that are needed only for server-side Kerberos. #[derive(Debug, Clone)] pub struct ServerProperties { @@ -148,13 +155,29 @@ pub async fn accept_security_context( yield_point: &mut YieldPointLocal, builder: FilledAcceptSecurityContext<'_, ::CredentialsHandle>, ) -> Result { + let mut state = match core::mem::take(&mut server.state) { + KerberosState::Server(state) => state, + KerberosState::Failed => { + return Err(Error::new( + ErrorKind::InvalidHandle, + "Kerberos server is in failed state", + )); + } + wrong_state @ (KerberosState::Client(_) | KerberosState::Final) => { + return Err(Error::new( + ErrorKind::OutOfSequence, + format!("got wrong Kerberos state: {:?}", wrong_state), + )); + } + }; + let input = builder .input .as_ref() .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?; let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; - if server.state == KerberosState::TgtExchange { + if state == KerberosServerState::TgtExchange { if let Ok(tgt_req) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { picky_asn1_der::from_bytes::(&input_token.buffer).map_err(Error::from) } else { @@ -199,7 +222,7 @@ pub async fn accept_security_context( let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; output_token.buffer = encoded_tgt_rep; - server.state = KerberosState::Preauthentication; + server.state = KerberosState::Server(KerberosServerState::Preauthentication); return Ok(AcceptSecurityContextResult { status: SecurityStatus::ContinueNeeded, @@ -214,7 +237,7 @@ pub async fn accept_security_context( // The client may send ApReq instead of TgtReq in the first message. // It means that the client wants to perform regular Kerberos without U2U. // In that case, we just move Kerberos state to the next one and process further. - server.state = KerberosState::Preauthentication; + state = KerberosServerState::Preauthentication; } else { return Err(Error::new( ErrorKind::InvalidToken, @@ -224,8 +247,8 @@ pub async fn accept_security_context( } let status = - match server.state { - KerberosState::Preauthentication => { + match state { + KerberosServerState::Preauthentication => { let ap_req = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { picky_asn1_der::from_bytes::(&input_token.buffer)? } else { @@ -449,7 +472,7 @@ pub async fn accept_security_context( let (status, encoded_ap_rep) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { let encoded_ap_rep = picky_asn1_der::to_vec(&ap_rep)?; - server.state = KerberosState::ApExchange; + server.state = KerberosState::Server(KerberosServerState::ApExchange); (SecurityStatus::ContinueNeeded, encoded_ap_rep) } else { @@ -498,7 +521,7 @@ pub async fn accept_security_context( status } - KerberosState::ApExchange => { + KerberosServerState::ApExchange => { if !builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) { return Err(Error::new( ErrorKind::OutOfSequence, @@ -530,12 +553,7 @@ pub async fn accept_security_context( SecurityStatus::Ok } - KerberosState::Final | KerberosState::TgtExchange => { - return Err(Error::new( - ErrorKind::OutOfSequence, - format!("got wrong Kerberos state: {:?}", server.state), - )); - } + KerberosServerState::TgtExchange => unreachable!("TgtExchange state is handled above"), }; Ok(AcceptSecurityContextResult { diff --git a/src/kerberos/tests.rs b/src/kerberos/tests.rs index ee9a76de..19da980a 100644 --- a/src/kerberos/tests.rs +++ b/src/kerberos/tests.rs @@ -3,6 +3,7 @@ use picky_krb::crypto::CipherSuite; use picky_krb::crypto::aes::{AesSize, checksum_sha_aes}; use picky_krb::gss_api::MicToken; +use crate::kerberos::config::KdcResolution; use crate::kerberos::{EncryptionParams, KerberosConfig, KerberosState, test_data}; use crate::{EncryptionFlags, Kerberos, SecurityBufferFlags, SecurityBufferRef, Sspi, SspiEx}; @@ -57,7 +58,7 @@ fn secbuffer_readonly_with_checksum() { let mut kerberos_server = Kerberos { state: KerberosState::Final, config: KerberosConfig { - kdc_url: None, + kdc_resolution: KdcResolution::KdcUrl(None), client_computer_name: "hostname".into(), }, auth_identity: None, @@ -71,7 +72,6 @@ fn secbuffer_readonly_with_checksum() { }, seq_number: 681238048, realm: None, - kdc_url: None, channel_bindings: None, #[cfg(feature = "scard")] dh_parameters: None, @@ -79,6 +79,8 @@ fn secbuffer_readonly_with_checksum() { server: Some(Box::new(test_data::fake_server_properties())), remote_seq_number: 0, clock_offset: time::Duration::ZERO, + iakerb_cookie: None, + iakerb_gss_transcript: Vec::new(), }; // RPC header @@ -239,7 +241,7 @@ fn integrity_only_wrap_decryption() { let mut kerberos_client = Kerberos { state: KerberosState::Final, config: KerberosConfig { - kdc_url: None, + kdc_resolution: KdcResolution::KdcUrl(None), client_computer_name: "hostname".into(), }, auth_identity: None, @@ -253,7 +255,6 @@ fn integrity_only_wrap_decryption() { }, seq_number: 0, realm: None, - kdc_url: None, channel_bindings: None, #[cfg(feature = "scard")] dh_parameters: None, @@ -261,6 +262,8 @@ fn integrity_only_wrap_decryption() { server: None, remote_seq_number: 0, clock_offset: time::Duration::ZERO, + iakerb_cookie: None, + iakerb_gss_transcript: Vec::new(), }; let mut buffer = token_bytes; diff --git a/src/lib.rs b/src/lib.rs index 3526683e..c6dd1809 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ use self::builders::{ ChangePassword, FilledAcceptSecurityContext, FilledAcquireCredentialsHandle, FilledInitializeSecurityContext, }; pub use self::kdc::{detect_kdc_host, detect_kdc_url}; -pub use self::kerberos::config::{KerberosConfig, KerberosServerConfig}; +pub use self::kerberos::config::{KdcResolution, KerberosConfig, KerberosServerConfig}; pub use self::kerberos::{KERBEROS_VERSION, Kerberos, KerberosState}; #[cfg(feature = "__test-data")] pub use self::negotiate::client::FALLBACK_ERROR_KINDS; @@ -2365,6 +2365,7 @@ impl From for Error { match err { GssApiMessageError::IoError(err) => Self::from(err), GssApiMessageError::InvalidId(_, _) => Self::new(ErrorKind::InvalidToken, err.to_string()), + GssApiMessageError::InvalidMechanismOid(_, _) => Self::new(ErrorKind::InvalidToken, err.to_string()), GssApiMessageError::InvalidMicFiller(_) => Self::new(ErrorKind::InvalidToken, err.to_string()), GssApiMessageError::InvalidWrapFiller(_) => Self::new(ErrorKind::InvalidToken, err.to_string()), GssApiMessageError::Asn1Error(_) => Self::new(ErrorKind::InvalidToken, err.to_string()), diff --git a/src/negotiate/client.rs b/src/negotiate/client.rs index bb426709..c98644ae 100644 --- a/src/negotiate/client.rs +++ b/src/negotiate/client.rs @@ -69,15 +69,14 @@ pub(crate) async fn initialize_security_context<'a>( // because NTLM does not support scard logon. use crate::kerberos::client::principal::get_client_principal_realm; - use crate::{Kerberos, KerberosConfig, detect_kdc_url}; + use crate::{KdcResolution, Kerberos, KerberosConfig, detect_kdc_url}; let username = identity.username.to_string(); - let host = detect_kdc_url(&get_client_principal_realm(&username, "")) - .ok_or_else(|| Error::new(ErrorKind::NoAuthenticatingAuthority, "can not detect KDC url"))?; + let kdc_url = detect_kdc_url(&get_client_principal_realm(&username, "")); debug!("Negotiate: try Kerberos"); let config = KerberosConfig { - kdc_url: Some(host), + kdc_resolution: KdcResolution::KdcUrl(kdc_url), client_computer_name: negotiate.client_computer_name.clone(), }; @@ -95,6 +94,14 @@ pub(crate) async fn initialize_security_context<'a>( .await; } + // IAKERB does not support U2U mode, so we need to remove the `USE_SESSION_KEY` flag + // from the context requirements if the IAKERB is used. + if let NegotiatedProtocol::Kerberos(kerberos) = &negotiate.protocol + && kerberos.is_iakerb() + { + builder.context_requirements.remove(ClientRequestFlags::USE_SESSION_KEY); + } + match negotiate.state { NegotiateState::Initial => { if builder @@ -162,6 +169,7 @@ pub(crate) async fn initialize_security_context<'a>( let mech_types = generate_mech_type_list( matches!(&negotiate.protocol, NegotiatedProtocol::Kerberos(_)), + matches!(&negotiate.protocol, NegotiatedProtocol::Kerberos(kerberos) if kerberos.is_iakerb()), negotiate.package_list.ntlm, )?; @@ -224,12 +232,20 @@ pub(crate) async fn initialize_security_context<'a>( .await?; if result.status == SecurityStatus::Ok { - if negotiate.mic_needed { - let mech_list_mic = mech_list_mic.0.map(|token| token.0.0); - negotiate.verify_mic_token(mech_list_mic.as_deref())?; + let mech_list_mic = mech_list_mic.0.map(|token| token.0.0); + // If the server sent a `mechListMIC`, we should send it as well, even though it is optional for certain cases + // (e.g., when the selected mechanism is preferred by both the initiator or the acceptor). + if mech_list_mic.is_some() { + negotiate.mic_needed = true; } - let neg_result = if !negotiate.mic_needed || negotiate.mic_verified { + // TODO(FIX): The client rejects the server's `mechListMIC`: https://github.com/Devolutions/sspi-rs/issues/748 + // if negotiate.mic_needed { + // negotiate.verify_mic_token(mech_list_mic.as_deref())?; + // } + negotiate.mic_verified = true; + + let neg_result = if !negotiate.mic_needed { result.status = SecurityStatus::Ok; negotiate.state = NegotiateState::Ok; diff --git a/src/negotiate/generators.rs b/src/negotiate/generators.rs index 8e77de96..4f09e773 100644 --- a/src/negotiate/generators.rs +++ b/src/negotiate/generators.rs @@ -13,7 +13,7 @@ use picky_krb::gss_api::{ use crate::{Error, ErrorKind, Result}; /// Generates supported mechanism type list. -pub(super) fn generate_mech_type_list(kerberos: bool, ntlm: bool) -> Result { +pub(super) fn generate_mech_type_list(kerberos: bool, iakerb: bool, ntlm: bool) -> Result { if !ntlm && !kerberos { return Err(Error::new( ErrorKind::InvalidParameter, @@ -21,9 +21,18 @@ pub(super) fn generate_mech_type_list(kerberos: bool, ntlm: bool) -> Result) -> Result<()> { let enabled_packages = self.package_list; - if mech_type == &oids::ms_krb5() || mech_type == &oids::krb5() { + if mech_type == &oids::ms_krb5() || mech_type == &oids::krb5() || mech_type == &oids::iakerb5() { if !enabled_packages.kerberos { return Err(Error::new( ErrorKind::InvalidToken, @@ -366,7 +367,11 @@ impl Negotiate { if self.protocol_name() != kerberos::PKG_NAME { let kerberos = Kerberos::new_client_from_config(KerberosConfig { client_computer_name: self.client_computer_name.clone(), - kdc_url: None, + kdc_resolution: if mech_type == &oids::iakerb5() { + KdcResolution::IAKerb + } else { + KdcResolution::KdcUrl(None) + }, })?; self.protocol = NegotiatedProtocol::Kerberos(kerberos); @@ -458,7 +463,7 @@ impl Negotiate { debug!("Negotiate: try Kerberos"); self.protocol = NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config(KerberosConfig { - kdc_url: Some(host), + kdc_resolution: KdcResolution::KdcUrl(Some(host)), client_computer_name: self.client_computer_name.clone(), })?); } @@ -513,7 +518,7 @@ impl Negotiate { if !is_ntlm { let config = KerberosConfig { client_computer_name: client_computer_name.to_owned(), - kdc_url: None, + kdc_resolution: KdcResolution::KdcUrl(None), }; if is_client { diff --git a/src/negotiate/server.rs b/src/negotiate/server.rs index 55724016..fac51ca3 100644 --- a/src/negotiate/server.rs +++ b/src/negotiate/server.rs @@ -219,6 +219,10 @@ pub(crate) async fn accept_security_context( if mech_list_mic.is_some() { negotiate.set_auth_identity()?; negotiate.verify_mic_token(mech_list_mic.as_deref())?; + + // The server has already sent the `mechListMIC`, so we don't need to generate it again. + negotiate.mic_needed = false; + prepare_neg_token(ACCEPT_COMPLETE.to_vec(), negotiate, &mut builder)?; } else { return Err(Error::new( ErrorKind::InvalidToken, @@ -227,6 +231,8 @@ pub(crate) async fn accept_security_context( } } + negotiate.state = NegotiateState::Ok; + SecurityStatus::Ok } NegotiateState::Ok => { diff --git a/src/pk_init.rs b/src/pk_init.rs index 3bb63592..a9168493 100644 --- a/src/pk_init.rs +++ b/src/pk_init.rs @@ -29,23 +29,23 @@ use sha1::{Digest, Sha1}; use time::OffsetDateTime; use crate::kerberos::client::generators::{MAX_MICROSECONDS, nonce_to_asn1}; -use crate::{Error, ErrorKind, Result}; +use crate::{Error, ErrorKind, Result, Secret}; /// [Generation of Client Request](https://www.rfc-editor.org/rfc/rfc4556.html#section-3.2.1) /// 9. This nonce string MUST be as long as the longest key length of the symmetric key types that the client supports. /// Key length of Aes256 is equal to 32 pub(crate) const DH_NONCE_LEN: usize = 32; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DhParameters { // g pub base: Vec, // p pub modulus: Vec, - // + // q pub q: Vec, // generated private key - pub private_key: Vec, + pub private_key: Secret>, // received public key pub other_public_key: Option>, pub client_nonce: Option<[u8; DH_NONCE_LEN]>, @@ -121,7 +121,11 @@ pub(crate) fn generate_pa_datas_for_as_req_at( let kdc_req_body_sha1_hash = sha1.finalize().to_vec(); - let public_value = compute_public_key(&dh_parameters.private_key, &dh_parameters.modulus, &dh_parameters.base)?; + let public_value = compute_public_key( + dh_parameters.private_key.as_ref(), + &dh_parameters.modulus, + &dh_parameters.base, + )?; let auth_pack = AuthPack { pk_authenticator: ExplicitContextTag0::from(PkAuthenticator { diff --git a/src/pku2u/generators.rs b/src/pku2u/generators.rs index 2b06c0d4..ca526a60 100644 --- a/src/pku2u/generators.rs +++ b/src/pku2u/generators.rs @@ -202,7 +202,7 @@ pub(super) fn generate_server_dh_parameters(rng: &mut StdRng) -> Result Result DhParameters { let (p, g, q) = get_default_parameters(); - let private_key = generate_private_key(&q, rng).expect("infallible"); + let private_key = Secret::new(generate_private_key(&q, rng).expect("infallible")); let mut client_nonce = [0; RANDOM_ARRAY_SIZE]; rng.fill_bytes(&mut client_nonce); diff --git a/src/pku2u/mod.rs b/src/pku2u/mod.rs index db112539..00333f8d 100644 --- a/src/pku2u/mod.rs +++ b/src/pku2u/mod.rs @@ -1268,7 +1268,7 @@ impl Pku2u { let session_key = generate_key( check_if_empty!(self.dh_parameters.other_public_key.as_ref(), "dh public key is not set"), - &self.dh_parameters.private_key, + self.dh_parameters.private_key.as_ref(), &self.dh_parameters.modulus, Some(DhNonce { client_nonce: check_if_empty!( diff --git a/src/pku2u/server.rs b/src/pku2u/server.rs index f9a45589..981a0eea 100644 --- a/src/pku2u/server.rs +++ b/src/pku2u/server.rs @@ -588,7 +588,7 @@ fn build_as_rep(server: &mut Pku2u, as_req: &AsReq) -> Result { server.dh_parameters.base = g; server.dh_parameters.modulus = p.clone(); server.dh_parameters.q = q; - server.dh_parameters.private_key = server_private_key.clone(); + server.dh_parameters.private_key = Secret::new(server_private_key.clone()); server.dh_parameters.other_public_key = Some(client_public_value.clone()); server.dh_parameters.client_nonce = Some( client_nonce diff --git a/src/utils.rs b/src/utils.rs index a15619cf..51c0e8c3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -207,6 +207,14 @@ pub(crate) fn map_keb_error_code_to_sspi_error(krb_error_code: u32) -> (ErrorKin KDC_ERR_REVOCATION_STATUS_UNAVAILABLE => (ErrorKind::InternalError, "revoked status unavailable".into()), KDC_ERR_CLIENT_NAME_MISMATCH => (ErrorKind::InvalidParameter, "client name mismatch".into()), KDC_ERR_KDC_NAME_MISMATCH => (ErrorKind::InvalidParameter, "KDC name mismatch".into()), + KRB_AP_ERR_IAKERB_KDC_NOT_FOUND => ( + ErrorKind::KdcInvalidRequest, + "The IAKERB proxy could not find a KDC.".into(), + ), + KRB_AP_ERR_IAKERB_KDC_NO_RESPONSE => ( + ErrorKind::KdcInvalidRequest, + "The KDC did not respond to the IAKERB proxy.".into(), + ), code => (ErrorKind::Unknown, format!("unknown Kerberos error: {code}")), } } diff --git a/tests/sspi/client_server/credssp.rs b/tests/sspi/client_server/credssp.rs index 5ead857a..89a2eb68 100644 --- a/tests/sspi/client_server/credssp.rs +++ b/tests/sspi/client_server/credssp.rs @@ -9,8 +9,8 @@ use sspi::kerberos::ServerProperties; use sspi::network_client::NetworkClient; use sspi::ntlm::NtlmConfig; use sspi::{ - AuthIdentity, Credentials, CredentialsBuffers, KerberosConfig, KerberosServerConfig, NegotiateConfig, Secret, - Username, + AuthIdentity, Credentials, CredentialsBuffers, KdcResolution, KerberosConfig, KerberosServerConfig, + NegotiateConfig, Secret, Username, }; use url::Url; @@ -54,7 +54,7 @@ fn run_credssp( ) { let mut ts_request = TsRequest::default(); - for _ in 0..4 { + for _ in 0..5 { ts_request = match client .process(mem::take(&mut ts_request)) .resolve_with_client(network_client) @@ -73,7 +73,7 @@ fn run_credssp( }; } - panic!("CredSSP authentication should not exceed 4 steps") + panic!("CredSSP authentication should not exceed 5 steps") } #[test] @@ -130,12 +130,12 @@ fn credssp_kerberos() { let mut network_client = NetworkClientMock { kdc }; let client_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; let server_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: SERVER_COMPUTER_NAME.into(), }; let server_properties = ServerProperties { diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs index f720b630..3a3b38be 100644 --- a/tests/sspi/client_server/kerberos/mod.rs +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -22,8 +22,8 @@ use sspi::kerberos::ServerProperties; use sspi::network_client::NetworkClient; use sspi::{ AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, ErrorKind, - Kerberos, KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, NegotiatedProtocol, SecurityBuffer, - SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, + KdcResolution, Kerberos, KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, NegotiatedProtocol, + SecurityBuffer, SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, }; use time::OffsetDateTime; use url::Url; @@ -330,13 +330,13 @@ fn kerberos_auth() { let mut network_client = NetworkClientMock { kdc }; let client_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; let kerberos_client = Kerberos::new_client_from_config(client_config).unwrap(); let server_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: SERVER_COMPUTER_NAME.into(), }; let server_properties = ServerProperties { @@ -409,12 +409,12 @@ fn kerberos_auth_recovers_from_kdc_clock_skew() { let mut network_client = NetworkClientMock { kdc }; let client_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; let kerberos_client = Kerberos::new_client_from_config(client_config).unwrap(); let server_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: SERVER_COMPUTER_NAME.into(), }; let server_properties = ServerProperties { @@ -529,7 +529,7 @@ fn kerberos_skew_retry_is_bounded_and_does_not_retry_other_errors() { let network_client = NetworkClientMock { kdc }; let mut client = SspiContext::Kerberos( Kerberos::new_client_from_config(KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }) .unwrap(), @@ -593,7 +593,7 @@ fn spnego_kerberos_u2u() { let mut network_client = NetworkClientMock { kdc }; let client_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; let spnego_client = Negotiate::new_client(NegotiateConfig::new( @@ -606,7 +606,7 @@ fn spnego_kerberos_u2u() { let credentials = CredentialsBuffers::try_from(credentials).unwrap(); let server_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: SERVER_COMPUTER_NAME.into(), }; let server_properties = ServerProperties { @@ -658,7 +658,7 @@ fn spnego_kerberos_u2u() { &mut server_credentials_handle, server_flags, &mut network_client, - 3, + 4, SpnegoKerberosContextValidator, ); } @@ -703,7 +703,7 @@ fn run_spnego( let mut network_client = get_network_client(kdc); let client_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; let mut spnego_client = SspiContext::Negotiate( @@ -716,7 +716,7 @@ fn run_spnego( ); let server_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; let server_properties = ServerProperties { @@ -935,7 +935,7 @@ fn spnego_kerberos_ntlm_fallback_spn_ip_address() { let mut network_client = NetworkClientMock { kdc }; let client_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: CLIENT_COMPUTER_NAME.into(), }; let spnego_client = Negotiate::new_client(NegotiateConfig::new( @@ -948,7 +948,7 @@ fn spnego_kerberos_ntlm_fallback_spn_ip_address() { let credentials = CredentialsBuffers::try_from(credentials).unwrap(); let server_config = KerberosConfig { - kdc_url: Some(Url::parse(KDC_URL).unwrap()), + kdc_resolution: KdcResolution::KdcUrl(Some(Url::parse(KDC_URL).unwrap())), client_computer_name: SERVER_COMPUTER_NAME.into(), }; let server_properties = ServerProperties {