feat(core): RDP client on IronRDP
clients::rdp connects over any caller-dialled TCP stream (direct, proxy or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM), optionally falling back to TLS-only logon for servers without NLA such as xrdp (credentials are passed so they log straight in). The server certificate is pinned through a caller check on its SHA256 fingerprint before any credentials are sent. The desktop decodes into the VNC client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse operations), Display Control resizes and the deactivation-reactivation sequence run on the session thread. IronRDP's releases need three vendored patches (vendor/README.md): picky and sspi with their RustCrypto release-candidate pins relaxed so they share russh's stable versions (their own suites, 402 tests, pass against them), and ironrdp-session with two unreleased upstream bitmap fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops render sheared. Verified live against xrdp (scottyhardy/docker-remote-desktop): logon, full desktop rendering, keyboard and mouse input.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
use picky_krb::data_types::{KrbResult, ResultExt};
|
||||
use picky_krb::messages::{AsRep, KdcReqBody};
|
||||
|
||||
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};
|
||||
|
||||
/// Performs AS exchange as specified in [RFC 4210: The Authentication Service Exchange](https://www.rfc-editor.org/rfc/rfc4120#section-3.1).
|
||||
pub(crate) async fn as_exchange(
|
||||
client: &mut Kerberos,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
kdc_req_body: &KdcReqBody,
|
||||
mut pa_data_options: AsReqPaDataOptions<'_>,
|
||||
) -> Result<AsRep> {
|
||||
pa_data_options.with_pre_auth(false);
|
||||
let pa_datas = pa_data_options.generate()?;
|
||||
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<AsRep> = 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 pa_datas = pa_data_options.generate()?;
|
||||
|
||||
let as_req = generate_as_req(pa_datas, kdc_req_body.clone());
|
||||
|
||||
let response = client.send(yield_point, &serialize_message(&as_req)?).await?;
|
||||
|
||||
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..]);
|
||||
|
||||
Ok(KrbResult::<AsRep>::deserialize(&mut d)?.inspect_err(|err| error!(?err, "AS exchange error"))?)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
use picky_krb::messages::KrbPrivMessage;
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::{Rng as _, SeedableRng as _};
|
||||
|
||||
use crate::builders::ChangePassword;
|
||||
use crate::generator::YieldPointLocal;
|
||||
use crate::kerberos::client::extractors::{
|
||||
extract_encryption_params_from_as_rep, extract_session_key_from_as_rep, extract_status_code_from_krb_priv_response,
|
||||
};
|
||||
use crate::kerberos::client::generators::{
|
||||
EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, GenerateAuthenticatorOptions, generate_as_req_kdc_body,
|
||||
generate_authenticator, generate_krb_priv_request,
|
||||
};
|
||||
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::utils::generate_random_symmetric_key;
|
||||
use crate::{ClientRequestFlags, Error, ErrorKind, Kerberos, Result};
|
||||
|
||||
/// [Kerberos Change Password and Set Password Protocols](https://datatracker.ietf.org/doc/html/rfc3244#section-2)
|
||||
/// "The service accepts requests on UDP port 464 and TCP port 464 as well."
|
||||
const KPASSWD_PORT: u16 = 464;
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?client.state), skip(client, change_password))]
|
||||
pub async fn change_password<'a>(
|
||||
client: &'a mut Kerberos,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
change_password: ChangePassword<'a>,
|
||||
) -> Result<()> {
|
||||
let username = &change_password.account_name;
|
||||
let domain = &change_password.domain_name;
|
||||
let password = &change_password.old_password;
|
||||
|
||||
let salt = format!("{domain}{username}");
|
||||
|
||||
let cname_type = get_client_principal_name_type(username, domain);
|
||||
let realm = &get_client_principal_realm(username, domain);
|
||||
|
||||
let mut rand = StdRng::try_from_rng(&mut SysRng)?;
|
||||
let nonce = &rand.next_u32().to_ne_bytes();
|
||||
|
||||
let options = GenerateAsReqOptions {
|
||||
realm,
|
||||
username,
|
||||
cname_type,
|
||||
snames: &[KADMIN, CHANGE_PASSWORD_SERVICE_NAME],
|
||||
// 4 = size of u32
|
||||
nonce,
|
||||
hostname: &client.config.client_computer_name,
|
||||
context_requirements: ClientRequestFlags::empty(),
|
||||
};
|
||||
let kdc_req_body = generate_as_req_kdc_body(&options)?;
|
||||
|
||||
let pa_data_options = AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions {
|
||||
password: password.as_ref(),
|
||||
salt: salt.into_bytes(),
|
||||
enc_params: client.encryption_params.clone(),
|
||||
with_pre_auth: false,
|
||||
});
|
||||
|
||||
let as_rep = client::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)?;
|
||||
debug!(?encryption_type, "Negotiated encryption type");
|
||||
|
||||
client.encryption_params.encryption_type = Some(CipherSuite::try_from(usize::from(encryption_type))?);
|
||||
|
||||
let session_key = extract_session_key_from_as_rep(&as_rep, &salt, password.as_ref(), &client.encryption_params)?;
|
||||
|
||||
let seq_num = client.next_seq_number();
|
||||
|
||||
let enc_type = client
|
||||
.encryption_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
|
||||
let authenticator_seb_key = generate_random_symmetric_key(enc_type, &mut rand);
|
||||
|
||||
let authenticator = generate_authenticator(GenerateAuthenticatorOptions {
|
||||
kdc_rep: &as_rep.0,
|
||||
seq_num: Some(seq_num),
|
||||
sub_key: Some(EncKey {
|
||||
key_type: enc_type.clone(),
|
||||
key_value: authenticator_seb_key,
|
||||
}),
|
||||
checksum: None,
|
||||
channel_bindings: client.channel_bindings.as_ref(),
|
||||
extensions: Vec::new(),
|
||||
})?;
|
||||
|
||||
let krb_priv = generate_krb_priv_request(
|
||||
as_rep.0.ticket.0,
|
||||
&session_key,
|
||||
change_password.new_password.as_ref().as_bytes(),
|
||||
&authenticator,
|
||||
&client.encryption_params,
|
||||
seq_num,
|
||||
&client.config.client_computer_name,
|
||||
)?;
|
||||
|
||||
if let Some((_realm, mut kdc_url)) = client.get_kdc() {
|
||||
kdc_url
|
||||
.set_port(Some(KPASSWD_PORT))
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidParameter, "Cannot set port for KDC URL"))?;
|
||||
|
||||
let response = client.send(yield_point, &serialize_message(&krb_priv)?).await?;
|
||||
trace!(?response, "Change password raw response");
|
||||
|
||||
if response.len() < 4 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InternalError,
|
||||
"the KDC reply message is too small: expected at least 4 bytes",
|
||||
));
|
||||
}
|
||||
|
||||
let krb_priv_response = KrbPrivMessage::deserialize(&response[4..]).map_err(|err| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!("cannot deserialize krb_priv_response: {err:?}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let result_status = extract_status_code_from_krb_priv_response(
|
||||
&krb_priv_response.krb_priv,
|
||||
&authenticator.0.subkey.0.as_ref().unwrap().0.key_value.0.0,
|
||||
&client.encryption_params,
|
||||
)?;
|
||||
|
||||
if result_status != 0 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::WrongCredentialHandle,
|
||||
format!("unsuccessful krb result code: {result_status}. expected 0"),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
ErrorKind::NoAuthenticatingAuthority,
|
||||
"no KDC server found".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
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::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;
|
||||
use picky_krb::data_types::{EncApRepPart, EncKrbPrivPart, EtypeInfo2, PaData, Ticket};
|
||||
use picky_krb::messages::{ApRep, AsRep, EncAsRepPart, EncTgsRepPart, KrbError, KrbPriv, TgsRep, TgtRep};
|
||||
|
||||
use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EncryptionParams};
|
||||
use crate::{Error, ErrorKind, Result, Secret};
|
||||
|
||||
/// Extracts password salt from the KRB error.
|
||||
///
|
||||
/// We need a salt to derive the correct encryption key from user's password. Usually, the salt is domain+username, but the custom salt
|
||||
/// value can be set in KDC database. So, we always extract the correct salt from the [KrbError] message. More info in [RFC 4120 PA-ETYPE-INFO2](https://www.rfc-editor.org/rfc/rfc4120#section-5.2.7.5):
|
||||
///
|
||||
/// > The ETYPE-INFO2 pre-authentication type is sent by the KDC in a KRB-ERROR indicating a requirement for additional pre-authentication.
|
||||
/// > It is usually used to notify a client of which key to use for the encryption of an encrypted timestamp for the purposes of sending a
|
||||
/// > PA-ENC-TIMESTAMP pre-authentication value.
|
||||
pub fn extract_salt_from_krb_error(error: &KrbError) -> Result<Option<String>> {
|
||||
trace!(?error, "KRB_ERROR");
|
||||
|
||||
if let Some(e_data) = error.0.e_data.0.as_ref() {
|
||||
let pa_datas: Asn1SequenceOf<PaData> = picky_asn1_der::from_bytes(&e_data.0.0)?;
|
||||
|
||||
if let Some(pa_etype_info_2) = pa_datas
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|pa_data| pa_data.padata_type.0.0 == PA_ETYPE_INFO2_TYPE)
|
||||
{
|
||||
let etype_info_2: EtypeInfo2 = picky_asn1_der::from_bytes(&pa_etype_info_2.padata_data.0.0)?;
|
||||
if let Some(params) = etype_info_2.0.first() {
|
||||
return Ok(params.salt.0.as_ref().map(|salt| salt.0.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Decrypts the AS-REP enc-part with an already-derived long-term `key` and
|
||||
/// returns the embedded session key.
|
||||
fn decode_as_rep_session_key(as_rep: &AsRep, key: &[u8], enc_params: &EncryptionParams) -> Result<Secret<Vec<u8>>> {
|
||||
let cipher = enc_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher();
|
||||
|
||||
let enc_data = cipher.decrypt(key, AS_REP_ENC, &as_rep.0.enc_part.0.cipher.0.0)?;
|
||||
|
||||
// This function extracts the session key from an AS-REP, so the enc-part is
|
||||
// expected to be tagged EncASRepPart (APPLICATION 25). We do not accept
|
||||
// EncTGSRepPart here: that tag belongs to the TGS exchange.
|
||||
let as_rep_enc_part = picky_asn1_der::from_bytes::<EncAsRepPart>(&enc_data)?;
|
||||
|
||||
Ok(as_rep_enc_part.0.key.0.key_value.0.to_vec().into())
|
||||
}
|
||||
|
||||
/// Extracts a session from the [AsRep].
|
||||
#[instrument(level = "trace", ret, skip(password))]
|
||||
pub fn extract_session_key_from_as_rep(
|
||||
as_rep: &AsRep,
|
||||
salt: &str,
|
||||
password: &str,
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<Secret<Vec<u8>>> {
|
||||
let cipher = enc_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher();
|
||||
|
||||
let key = cipher.generate_key_from_password(password.as_bytes(), salt.as_bytes())?;
|
||||
|
||||
decode_as_rep_session_key(as_rep, &key, enc_params)
|
||||
}
|
||||
|
||||
/// Extracts a session key from the [AsRep] using a pre-derived long-term key
|
||||
/// (keytab-based client authentication).
|
||||
#[instrument(level = "trace", ret, skip(key))]
|
||||
pub fn extract_session_key_from_as_rep_with_key(
|
||||
as_rep: &AsRep,
|
||||
key: &[u8],
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<Secret<Vec<u8>>> {
|
||||
decode_as_rep_session_key(as_rep, key, enc_params)
|
||||
}
|
||||
|
||||
/// Extracts a session from the [TgsRep].
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn extract_session_key_from_tgs_rep(
|
||||
tgs_rep: &TgsRep,
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<Secret<Vec<u8>>> {
|
||||
let cipher = enc_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher();
|
||||
|
||||
let enc_data = cipher
|
||||
.decrypt(
|
||||
session_key.as_ref(),
|
||||
TGS_REP_ENC_SESSION_KEY,
|
||||
&tgs_rep.0.enc_part.0.cipher.0.0,
|
||||
)
|
||||
.map_err(|e| Error::new(ErrorKind::DecryptFailure, format!("{e:?}")))?;
|
||||
|
||||
trace!(?enc_data, "Plain TgsRep::EncData");
|
||||
|
||||
let enc_as_rep_part: EncTgsRepPart = picky_asn1_der::from_bytes(&enc_data)?;
|
||||
|
||||
Ok(enc_as_rep_part.0.key.0.key_value.0.to_vec().into())
|
||||
}
|
||||
|
||||
/// Extracts encryption type and salt from [AsRep].
|
||||
///
|
||||
/// More info in [RFC 4120 Receipt of KRB_AS_REP Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.1.5):
|
||||
///
|
||||
/// > If any padata fields are present, they may be used to derive the proper secret key to decrypt the message.
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn extract_encryption_params_from_as_rep(as_rep: &AsRep) -> Result<(u8, String)> {
|
||||
match as_rep
|
||||
.0
|
||||
.padata
|
||||
.0
|
||||
.as_ref()
|
||||
.map(|v| {
|
||||
v.0.0
|
||||
.iter()
|
||||
.find(|e| e.padata_type.0.0 == PA_ETYPE_INFO2_TYPE)
|
||||
.map(|pa_data| pa_data.padata_data.0.0.clone())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(data) => {
|
||||
let pa_etype_info2: EtypeInfo2 = picky_asn1_der::from_bytes(&data)?;
|
||||
let pa_etype_info2 = pa_etype_info2
|
||||
.0
|
||||
.first()
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidParameter, "Missing EtypeInto2Entry in EtypeInfo2"))?;
|
||||
|
||||
Ok((
|
||||
pa_etype_info2.etype.0.0.first().copied().unwrap(),
|
||||
pa_etype_info2
|
||||
.salt
|
||||
.0
|
||||
.as_ref()
|
||||
.map(|salt| salt.0.to_string())
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidParameter, "Missing salt in EtypeInto2Entry"))?,
|
||||
))
|
||||
}
|
||||
None => Ok((*as_rep.0.enc_part.0.etype.0.0.first().unwrap(), Default::default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a status code from the [KrbPriv] message.
|
||||
pub fn extract_status_code_from_krb_priv_response(
|
||||
krb_priv: &KrbPriv,
|
||||
auth_key: &[u8],
|
||||
encryption_params: &EncryptionParams,
|
||||
) -> Result<u16> {
|
||||
let encryption_type = encryption_params
|
||||
.encryption_type
|
||||
.clone()
|
||||
.unwrap_or(CipherSuite::try_from(
|
||||
*krb_priv
|
||||
.0
|
||||
.enc_part
|
||||
.0
|
||||
.etype
|
||||
.0
|
||||
.0
|
||||
.first()
|
||||
.unwrap_or(&((&DEFAULT_ENCRYPTION_TYPE).into())) as usize,
|
||||
)?);
|
||||
|
||||
let cipher = encryption_type.cipher();
|
||||
|
||||
let enc_part: EncKrbPrivPart =
|
||||
picky_asn1_der::from_bytes(&cipher.decrypt(auth_key, KRB_PRIV_ENC_PART, &krb_priv.0.enc_part.0.cipher.0.0)?)?;
|
||||
let user_data = enc_part.0.user_data.0.0;
|
||||
|
||||
if user_data.len() < 2 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"Invalid KRB_PRIV message: user-data first is too short (expected at least 2 bytes)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(u16::from_be_bytes(user_data[0..2].try_into().unwrap()))
|
||||
}
|
||||
|
||||
/// Extracts the sequence number from the [ApRep].
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn extract_seq_number_from_ap_rep(
|
||||
ap_rep: &ApRep,
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cipher = enc_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher();
|
||||
|
||||
let res = cipher
|
||||
.decrypt(session_key.as_ref(), AP_REP_ENC, &ap_rep.0.enc_part.cipher.0.0)
|
||||
.map_err(|err| {
|
||||
Error::new(
|
||||
ErrorKind::DecryptFailure,
|
||||
format!("cannot decrypt ap_rep.enc_part: {err:?}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let ap_rep_enc_part: EncApRepPart = picky_asn1_der::from_bytes(&res)?;
|
||||
|
||||
Ok(ap_rep_enc_part
|
||||
.0
|
||||
.seq_number
|
||||
.0
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing sequence number in ap_rep"))?
|
||||
.0
|
||||
.0)
|
||||
}
|
||||
|
||||
/// Extracts a sub-session key from the [ApRep].
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn extract_sub_session_key_from_ap_rep(
|
||||
ap_rep: &ApRep,
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<Secret<Vec<u8>>> {
|
||||
let cipher = enc_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher();
|
||||
|
||||
let res = cipher
|
||||
.decrypt(session_key.as_ref(), AP_REP_ENC, &ap_rep.0.enc_part.cipher.0.0)
|
||||
.map_err(|err| {
|
||||
Error::new(
|
||||
ErrorKind::DecryptFailure,
|
||||
format!("cannot decrypt ap_rep.enc_part: {err:?}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let ap_rep_enc_part: EncApRepPart = picky_asn1_der::from_bytes(&res)?;
|
||||
|
||||
Ok(ap_rep_enc_part
|
||||
.0
|
||||
.subkey
|
||||
.0
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing sub-key in ap_req"))?
|
||||
.0
|
||||
.key_value
|
||||
.0
|
||||
.0
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Extracts TGT Ticket from encoded [NegTokenTarg1].
|
||||
///
|
||||
/// Returned OID means the selected authentication mechanism by the target server. More info:
|
||||
/// * [3.2.1. Syntax](https://datatracker.ietf.org/doc/html/rfc2478#section-3.2.1): `responseToken` field;
|
||||
///
|
||||
/// We use this oid to choose between the regular Kerberos 5 and Kerberos 5 User-to-User authentication.
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn extract_tgt_ticket_with_oid(mut resp_token: &[u8]) -> Result<Option<(Ticket, ObjectIdentifierAsn1)>> {
|
||||
if resp_token.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let oid: ApplicationTag<Asn1RawDer, 0> = picky_asn1_der::from_reader(&mut resp_token)?;
|
||||
let oid: ObjectIdentifierAsn1 = picky_asn1_der::from_bytes(&oid.0.0)?;
|
||||
|
||||
let mut t = [0, 0];
|
||||
|
||||
resp_token.read_exact(&mut t)?;
|
||||
|
||||
let tgt_rep: TgtRep = picky_asn1_der::from_reader(&mut resp_token)?;
|
||||
|
||||
Ok(Some((tgt_rep.ticket.0, oid)))
|
||||
}
|
||||
+938
@@ -0,0 +1,938 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitflags;
|
||||
use md5::{Digest, Md5};
|
||||
use picky_asn1::bit_string::BitString;
|
||||
use picky_asn1::date::GeneralizedTime;
|
||||
use picky_asn1::restricted_string::IA5String;
|
||||
use picky_asn1::wrapper::{
|
||||
Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3,
|
||||
ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, ExplicitContextTag7, ExplicitContextTag8,
|
||||
ExplicitContextTag9, ExplicitContextTag11, GeneralizedTimeAsn1, IntegerAsn1, OctetStringAsn1, Optional,
|
||||
};
|
||||
use picky_asn1_x509::oids;
|
||||
use picky_krb::constants::gss_api::AUTHENTICATOR_CHECKSUM_TYPE;
|
||||
use picky_krb::constants::key_usages::{
|
||||
AP_REP_ENC, AP_REQ_AUTHENTICATOR, KRB_PRIV_ENC_PART, TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR,
|
||||
};
|
||||
use picky_krb::constants::types::{
|
||||
AD_AUTH_DATA_AP_OPTION_TYPE, AP_REP_MSG_TYPE, AP_REQ_MSG_TYPE, AS_REQ_MSG_TYPE, KERB_AP_OPTIONS_CBT, KRB_PRIV,
|
||||
NET_BIOS_ADDR_TYPE, NT_SRV_INST, PA_ENC_TIMESTAMP, PA_ENC_TIMESTAMP_KEY_USAGE, PA_PAC_OPTIONS_TYPE,
|
||||
PA_PAC_REQUEST_TYPE, PA_TGS_REQ_TYPE, TGS_REQ_MSG_TYPE, TGT_REQ_MSG_TYPE,
|
||||
};
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
use picky_krb::data_types::{
|
||||
ApOptions, Authenticator, AuthenticatorInner, AuthorizationData, AuthorizationDataInner, Checksum, EncApRepPart,
|
||||
EncApRepPartInner, EncKrbPrivPart, EncKrbPrivPartInner, EncryptedData, EncryptionKey, HostAddress,
|
||||
KerbPaPacRequest, KerberosFlags, KerberosStringAsn1, KerberosTime, PaData, PaEncTsEnc, PaPacOptions, PrincipalName,
|
||||
Realm, Ticket,
|
||||
};
|
||||
use picky_krb::gss_api::{MechType, MechTypeList};
|
||||
use picky_krb::messages::{
|
||||
ApMessage, ApRep, ApRepInner, ApReq, ApReqInner, AsReq, KdcRep, KdcReq, KdcReqBody, KrbPriv, KrbPrivInner,
|
||||
KrbPrivMessage, TgsReq, TgtReq,
|
||||
};
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::{Rng as _, SeedableRng as _};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
use crate::channel_bindings::ChannelBindings;
|
||||
use crate::crypto::compute_md5_channel_bindings_hash;
|
||||
use crate::kerberos::flags::{ApOptions as ApOptionsFlags, KdcOptions};
|
||||
use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EncryptionParams, KERBEROS_VERSION};
|
||||
use crate::utils::parse_target_name;
|
||||
use crate::{ClientRequestFlags, Error, ErrorKind, Result, Secret};
|
||||
|
||||
/// Moved to [`super::principal::get_client_principal_name_type`].
|
||||
#[allow(
|
||||
clippy::deprecated_semver,
|
||||
reason = "`<next-version>` placeholder filled in at release time"
|
||||
)]
|
||||
#[deprecated(
|
||||
since = "<next-version>",
|
||||
note = "moved to the `kerberos::client::principal` module — see https://github.com/Devolutions/sspi-rs/issues/708"
|
||||
)]
|
||||
pub fn get_client_principal_name_type(username: &str, domain: &str) -> u8 {
|
||||
super::principal::get_client_principal_name_type(username, domain)
|
||||
}
|
||||
|
||||
/// Moved to [`super::principal::get_client_principal_realm`].
|
||||
#[allow(
|
||||
clippy::deprecated_semver,
|
||||
reason = "`<next-version>` placeholder filled in at release time"
|
||||
)]
|
||||
#[deprecated(
|
||||
since = "<next-version>",
|
||||
note = "moved to the `kerberos::client::principal` module — see https://github.com/Devolutions/sspi-rs/issues/708"
|
||||
)]
|
||||
pub fn get_client_principal_realm(username: &str, domain: &str) -> String {
|
||||
super::principal::get_client_principal_realm(username, domain)
|
||||
}
|
||||
|
||||
const TGT_TICKET_LIFETIME_DAYS: i64 = 3;
|
||||
const NONCE_LEN: usize = 4;
|
||||
/// [Microseconds](https://www.rfc-editor.org/rfc/rfc4120#section-5.2.4).
|
||||
/// The maximum microseconds value.
|
||||
///
|
||||
/// ```not_rust
|
||||
/// Microseconds ::= INTEGER (0..999999)
|
||||
/// ```
|
||||
pub const MAX_MICROSECONDS: u32 = 999_999;
|
||||
const MD5_CHECKSUM_TYPE: [u8; 1] = [0x07];
|
||||
|
||||
// Renewable, Canonicalize, and Renewable-ok are on by default
|
||||
// https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1
|
||||
pub const DEFAULT_AS_REQ_OPTIONS: [u8; 4] = [0x00, 0x81, 0x00, 0x10];
|
||||
|
||||
// Renewable, Canonicalize.
|
||||
// https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1
|
||||
const DEFAULT_TGS_REQ_OPTIONS: [u8; 4] = [0x00, 0x81, 0x00, 0x00];
|
||||
|
||||
const DEFAULT_PA_PAC_OPTIONS: [u8; 4] = [0x40, 0x00, 0x00, 0x00];
|
||||
|
||||
/// [Authenticator Checksum](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1)
|
||||
///
|
||||
/// **Important**: the last 4 bytes are [Checksum Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1).
|
||||
/// This value should be set separately based on provided [CLientRequestFlags] or [GssFlags].
|
||||
pub const AUTHENTICATOR_DEFAULT_CHECKSUM: [u8; 24] = [
|
||||
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
pub(super) fn generate_tgt_req(sname: &[&str]) -> Result<TgtReq> {
|
||||
let sname = sname
|
||||
.iter()
|
||||
.map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string(sname.to_string())?)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(TgtReq {
|
||||
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REQ_MSG_TYPE])),
|
||||
server_name: ExplicitContextTag2::from(PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(sname)),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Parameters for generating pa-datas for [AsReq] message.
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateAsPaDataOptions<'a> {
|
||||
pub password: &'a str,
|
||||
/// Salt for deriving the encryption key.
|
||||
///
|
||||
/// The salt value should be extracted from the [KrbError] message.
|
||||
pub salt: Vec<u8>,
|
||||
pub enc_params: EncryptionParams,
|
||||
/// Flag that indicates whether to generate pa-datas.
|
||||
pub with_pre_auth: bool,
|
||||
}
|
||||
|
||||
/// Build the PA-ENC-TIMESTAMP pre-auth value, encrypting the current time with
|
||||
/// an already-derived long-term `key` of type `encryption_type`.
|
||||
fn encode_enc_timestamp_pa_data(key: &[u8], encryption_type: &CipherSuite) -> Result<PaData> {
|
||||
let cipher = encryption_type.cipher();
|
||||
|
||||
let current_date = OffsetDateTime::now_utc();
|
||||
let microseconds = current_date.microsecond().min(MAX_MICROSECONDS);
|
||||
|
||||
let timestamp = PaEncTsEnc {
|
||||
patimestamp: ExplicitContextTag0::from(KerberosTime::from(GeneralizedTime::from(current_date))),
|
||||
pausec: Optional::from(Some(ExplicitContextTag1::from(IntegerAsn1::from(
|
||||
microseconds.to_be_bytes().to_vec(),
|
||||
)))),
|
||||
};
|
||||
let timestamp_bytes = picky_asn1_der::to_vec(×tamp)?;
|
||||
|
||||
trace!(?encryption_type, "AS timestamp encryption params",);
|
||||
|
||||
let encrypted_timestamp = cipher.encrypt(key, PA_ENC_TIMESTAMP_KEY_USAGE, ×tamp_bytes)?;
|
||||
|
||||
trace!(
|
||||
?current_date,
|
||||
?microseconds,
|
||||
?timestamp_bytes,
|
||||
?encrypted_timestamp,
|
||||
"Encrypted timestamp params",
|
||||
);
|
||||
|
||||
Ok(PaData {
|
||||
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ENC_TIMESTAMP.to_vec())),
|
||||
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&EncryptedData {
|
||||
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
|
||||
kvno: Optional::from(None),
|
||||
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_timestamp)),
|
||||
})?)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the PA-PAC-REQUEST pre-auth value (always requests a PAC).
|
||||
fn encode_pac_request_pa_data() -> Result<PaData> {
|
||||
Ok(PaData {
|
||||
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_PAC_REQUEST_TYPE.to_vec())),
|
||||
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&KerbPaPacRequest {
|
||||
include_pac: ExplicitContextTag0::from(true),
|
||||
})?)),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", ret, skip_all, fields(options.salt, options.enc_params, options.with_pre_auth))]
|
||||
pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions<'_>) -> Result<Vec<PaData>> {
|
||||
let GenerateAsPaDataOptions {
|
||||
password,
|
||||
salt,
|
||||
enc_params,
|
||||
with_pre_auth,
|
||||
} = options;
|
||||
|
||||
let mut pa_datas = Vec::new();
|
||||
|
||||
if *with_pre_auth {
|
||||
let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
|
||||
let key = encryption_type
|
||||
.cipher()
|
||||
.generate_key_from_password(password.as_bytes(), salt)?;
|
||||
pa_datas.push(encode_enc_timestamp_pa_data(&key, encryption_type)?);
|
||||
}
|
||||
|
||||
pa_datas.push(encode_pac_request_pa_data()?);
|
||||
|
||||
Ok(pa_datas)
|
||||
}
|
||||
|
||||
/// Parameters for generating pa-datas for an [AsReq] using a pre-derived
|
||||
/// long-term key (keytab-based client authentication).
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateKeytabPaDataOptions {
|
||||
/// Raw long-term key bytes.
|
||||
pub key: Secret<Vec<u8>>,
|
||||
/// Kerberos encryption type of `key` (e.g. aes256-cts-hmac-sha1-96).
|
||||
pub key_enctype: CipherSuite,
|
||||
/// Flag that indicates whether to generate the PA-ENC-TIMESTAMP pa-data.
|
||||
pub with_pre_auth: bool,
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", ret, skip_all, fields(options.key_enctype, options.with_pre_auth))]
|
||||
pub fn generate_pa_datas_for_as_req_with_key(options: &GenerateKeytabPaDataOptions) -> Result<Vec<PaData>> {
|
||||
let mut pa_datas = Vec::new();
|
||||
|
||||
if options.with_pre_auth {
|
||||
pa_datas.push(encode_enc_timestamp_pa_data(
|
||||
options.key.as_ref(),
|
||||
&options.key_enctype,
|
||||
)?);
|
||||
}
|
||||
|
||||
pa_datas.push(encode_pac_request_pa_data()?);
|
||||
|
||||
Ok(pa_datas)
|
||||
}
|
||||
|
||||
/// Parameters for generating [AsReq].
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateAsReqOptions<'a> {
|
||||
pub realm: &'a str,
|
||||
pub username: &'a str,
|
||||
pub cname_type: u8,
|
||||
pub snames: &'a [&'a str],
|
||||
pub nonce: &'a [u8],
|
||||
pub hostname: &'a str,
|
||||
pub context_requirements: ClientRequestFlags,
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn generate_as_req_kdc_body(options: &GenerateAsReqOptions<'_>) -> Result<KdcReqBody> {
|
||||
let GenerateAsReqOptions {
|
||||
realm,
|
||||
username,
|
||||
cname_type,
|
||||
snames,
|
||||
nonce,
|
||||
hostname: address,
|
||||
context_requirements,
|
||||
} = options;
|
||||
|
||||
let expiration_date = OffsetDateTime::now_utc()
|
||||
.checked_add(Duration::days(TGT_TICKET_LIFETIME_DAYS))
|
||||
.unwrap();
|
||||
|
||||
let host_address = HostAddress {
|
||||
addr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NET_BIOS_ADDR_TYPE])),
|
||||
address: ExplicitContextTag1::from(OctetStringAsn1::from(address.as_bytes().to_vec())),
|
||||
};
|
||||
|
||||
let address = Some(ExplicitContextTag9::from(Asn1SequenceOf::from(vec![host_address])));
|
||||
|
||||
let mut service_names = Vec::with_capacity(snames.len());
|
||||
for sname in *snames {
|
||||
service_names.push(KerberosStringAsn1::from(IA5String::from_string((*sname).to_owned())?));
|
||||
}
|
||||
|
||||
let mut as_req_options = KdcOptions::from_bits(u32::from_be_bytes(DEFAULT_AS_REQ_OPTIONS)).unwrap();
|
||||
if context_requirements.contains(ClientRequestFlags::DELEGATE) {
|
||||
as_req_options |= KdcOptions::FORWARDABLE;
|
||||
}
|
||||
|
||||
Ok(KdcReqBody {
|
||||
kdc_options: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes(
|
||||
as_req_options.bits().to_be_bytes().to_vec(),
|
||||
))),
|
||||
cname: Optional::from(Some(ExplicitContextTag1::from(PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![*cname_type])),
|
||||
// A Kerberos principal name is a sequence of `/`-separated
|
||||
// components (RFC 1964 §2.1.1). Service principals such as
|
||||
// `kafka/host` carry two components; user principals carry one.
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(
|
||||
username
|
||||
.split('/')
|
||||
.map(|c| Ok(KerberosStringAsn1::from(IA5String::from_string(c.to_owned())?)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
)),
|
||||
}))),
|
||||
realm: ExplicitContextTag2::from(Realm::from(IA5String::from_string((*realm).into())?)),
|
||||
sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(service_names)),
|
||||
}))),
|
||||
from: Optional::from(None),
|
||||
till: ExplicitContextTag5::from(GeneralizedTimeAsn1::from(GeneralizedTime::from(expiration_date))),
|
||||
rtime: Optional::from(Some(ExplicitContextTag6::from(GeneralizedTimeAsn1::from(
|
||||
GeneralizedTime::from(expiration_date),
|
||||
)))),
|
||||
nonce: ExplicitContextTag7::from(IntegerAsn1::from(nonce.to_vec())),
|
||||
etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![
|
||||
IntegerAsn1::from(vec![CipherSuite::Aes256CtsHmacSha196.into()]),
|
||||
IntegerAsn1::from(vec![CipherSuite::Aes128CtsHmacSha196.into()]),
|
||||
])),
|
||||
addresses: Optional::from(address),
|
||||
enc_authorization_data: Optional::from(None),
|
||||
additional_tickets: Optional::from(None),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, skip_all)]
|
||||
pub fn generate_as_req(pa_datas: Vec<PaData>, kdc_req_body: KdcReqBody) -> AsReq {
|
||||
AsReq::from(KdcReq {
|
||||
pvno: ExplicitContextTag1::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag2::from(IntegerAsn1::from(vec![AS_REQ_MSG_TYPE])),
|
||||
padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(pa_datas)))),
|
||||
req_body: ExplicitContextTag4::from(kdc_req_body),
|
||||
})
|
||||
}
|
||||
|
||||
/// Parameters for generating [TgsReq].
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateTgsReqOptions<'a> {
|
||||
pub realm: &'a str,
|
||||
pub service_principal: &'a str,
|
||||
pub session_key: &'a Secret<Vec<u8>>,
|
||||
/// [Ticket] extracted from the [AsRep] message.
|
||||
pub ticket: Ticket,
|
||||
/// [Authenticator] to be included in [TgsReq] pa-data.
|
||||
pub authenticator: &'a mut Authenticator,
|
||||
/// If the Kerberos U2U auth is negotiated, then this parameter must have one ticket: TGT ticket of the application service.
|
||||
/// Otherwise, set it to `None`.
|
||||
pub additional_tickets: Option<Vec<Ticket>>,
|
||||
pub enc_params: &'a EncryptionParams,
|
||||
pub context_requirements: ClientRequestFlags,
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret)]
|
||||
pub fn generate_tgs_req(options: GenerateTgsReqOptions<'_>) -> Result<TgsReq> {
|
||||
let GenerateTgsReqOptions {
|
||||
realm,
|
||||
service_principal,
|
||||
session_key,
|
||||
ticket,
|
||||
authenticator,
|
||||
additional_tickets,
|
||||
enc_params,
|
||||
context_requirements,
|
||||
} = options;
|
||||
|
||||
let (service_name, service_principal_name) = parse_target_name(service_principal)?;
|
||||
|
||||
let expiration_date = OffsetDateTime::now_utc()
|
||||
.checked_add(Duration::days(TGT_TICKET_LIFETIME_DAYS))
|
||||
.unwrap();
|
||||
|
||||
let mut tgs_req_options = KdcOptions::from_bits(u32::from_be_bytes(DEFAULT_TGS_REQ_OPTIONS)).unwrap();
|
||||
if context_requirements.contains(ClientRequestFlags::DELEGATE) {
|
||||
tgs_req_options |= KdcOptions::FORWARDABLE;
|
||||
}
|
||||
if context_requirements.contains(ClientRequestFlags::USE_SESSION_KEY) {
|
||||
tgs_req_options |= KdcOptions::ENC_TKT_IN_SKEY;
|
||||
}
|
||||
|
||||
let mut rng = StdRng::try_from_rng(&mut SysRng)?;
|
||||
let mut nonce = [0; NONCE_LEN];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let req_body = KdcReqBody {
|
||||
kdc_options: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes(
|
||||
tgs_req_options.bits().to_be_bytes().to_vec(),
|
||||
))),
|
||||
cname: Optional::from(None),
|
||||
realm: ExplicitContextTag2::from(Realm::from(IA5String::from_str(realm)?)),
|
||||
sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![
|
||||
KerberosStringAsn1::from(IA5String::from_string(service_name.into())?),
|
||||
KerberosStringAsn1::from(IA5String::from_string(service_principal_name.into())?),
|
||||
])),
|
||||
}))),
|
||||
from: Optional::from(None),
|
||||
till: ExplicitContextTag5::from(GeneralizedTimeAsn1::from(GeneralizedTime::from(expiration_date))),
|
||||
rtime: Optional::from(None),
|
||||
nonce: ExplicitContextTag7::from(IntegerAsn1::from(nonce.to_vec())),
|
||||
etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![
|
||||
IntegerAsn1::from(vec![CipherSuite::Aes256CtsHmacSha196.into()]),
|
||||
IntegerAsn1::from(vec![CipherSuite::Aes128CtsHmacSha196.into()]),
|
||||
])),
|
||||
addresses: Optional::from(None),
|
||||
enc_authorization_data: Optional::from(None),
|
||||
additional_tickets: Optional::from(
|
||||
additional_tickets.map(|tickets| ExplicitContextTag11::from(Asn1SequenceOf::from(tickets))),
|
||||
),
|
||||
};
|
||||
|
||||
let mut md5 = Md5::new();
|
||||
md5.update(&picky_asn1_der::to_vec(&req_body)?);
|
||||
let checksum = md5.finalize();
|
||||
|
||||
authenticator.0.cksum = Optional::from(Some(ExplicitContextTag3::from(Checksum {
|
||||
cksumtype: ExplicitContextTag0::from(IntegerAsn1::from(MD5_CHECKSUM_TYPE.to_vec())),
|
||||
checksum: ExplicitContextTag1::from(OctetStringAsn1::from(checksum.to_vec())),
|
||||
})));
|
||||
|
||||
let pa_tgs_req =
|
||||
PaData {
|
||||
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_TGS_REQ_TYPE.to_vec())),
|
||||
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(
|
||||
&generate_tgs_ap_req(ticket, session_key, authenticator, enc_params)?,
|
||||
)?)),
|
||||
};
|
||||
|
||||
let pa_pac_options = PaData {
|
||||
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_PAC_OPTIONS_TYPE.to_vec())),
|
||||
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&PaPacOptions {
|
||||
flags: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes(
|
||||
DEFAULT_PA_PAC_OPTIONS.to_vec(),
|
||||
))),
|
||||
})?)),
|
||||
};
|
||||
|
||||
Ok(TgsReq::from(KdcReq {
|
||||
pvno: ExplicitContextTag1::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag2::from(IntegerAsn1::from(vec![TGS_REQ_MSG_TYPE])),
|
||||
padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(vec![
|
||||
pa_tgs_req,
|
||||
pa_pac_options,
|
||||
])))),
|
||||
req_body: ExplicitContextTag4::from(req_body),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChecksumOptions {
|
||||
pub checksum_type: Vec<u8>,
|
||||
pub checksum_value: ChecksumValues,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChecksumValues {
|
||||
inner: Vec<u8>, // use named fields for future extensibility, this is a temporary solution
|
||||
}
|
||||
|
||||
impl Default for ChecksumValues {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: AUTHENTICATOR_DEFAULT_CHECKSUM.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChecksumValues> for Vec<u8> {
|
||||
fn from(val: ChecksumValues) -> Self {
|
||||
val.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 24]> for ChecksumValues {
|
||||
fn from(bytes: [u8; 24]) -> Self {
|
||||
ChecksumValues {
|
||||
inner: Vec::from(bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChecksumValues {
|
||||
pub(crate) fn set_flags(&mut self, flags: GssFlags) {
|
||||
let flag_bits = flags.bits();
|
||||
let flag_bytes = flag_bits.to_le_bytes();
|
||||
self.inner[20..24].copy_from_slice(&flag_bytes);
|
||||
}
|
||||
|
||||
pub(crate) fn into_inner(self) -> Vec<u8> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// The checksum "Flags" field is used to convey service options or extension negotiation information.
|
||||
/// More info:
|
||||
/// * https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1
|
||||
#[derive(Debug,Clone,Copy)]
|
||||
pub(crate) struct GssFlags: u32 {
|
||||
// [Checksum Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1).
|
||||
const GSS_C_DELEG_FLAG = 1;
|
||||
const GSS_C_MUTUAL_FLAG = 2;
|
||||
const GSS_C_REPLAY_FLAG = 4;
|
||||
const GSS_C_SEQUENCE_FLAG = 8;
|
||||
const GSS_C_CONF_FLAG = 16;
|
||||
const GSS_C_INTEG_FLAG = 32;
|
||||
|
||||
const GSS_C_ANON_FLAG = 64;
|
||||
const GSS_C_PROT_READY_FLAG = 128;
|
||||
const GSS_C_TRANS_FLAG = 256;
|
||||
const GSS_C_DELEG_POLICY_FLAG = 32768;
|
||||
|
||||
// Additional GSS flags from MS-KILE specification:
|
||||
// * [3.2.5.2 Authenticator Checksum Flags](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/387806fc-ed78-445e-afd8-c5639fe4a90a)
|
||||
|
||||
// [Mechanism Specific Changes](https://www.rfc-editor.org/rfc/rfc4757.html#section-7.1):
|
||||
// Setting this flag indicates that the client wants to be informed of extended error information. In
|
||||
// particular, Windows 2000 status codes may be returned in the data field of a Kerberos error message.
|
||||
// This allows the client to understand a server failure more precisely.
|
||||
const GSS_C_EXTENDED_ERROR_FLAG = 0x4000;
|
||||
// This flag allows the client to indicate to the server that it should only allow the server application to identify
|
||||
// the client by name and ID, but not to impersonate the client.
|
||||
const GSS_C_IDENTIFY_FLAG = 0x2000;
|
||||
// This flag was added for use with Microsoft's implementation of Distributed Computing Environment Remote Procedure
|
||||
// Call (DCE RPC), which initially expected three legs of authentication.
|
||||
const GSS_C_DCE_STYLE = 0x1000;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientRequestFlags> for GssFlags {
|
||||
/*
|
||||
the semantics of some of the flags of SSPI are I believe one to one mapped to the GSS flags
|
||||
*/
|
||||
fn from(value: ClientRequestFlags) -> Self {
|
||||
let mut flags = GssFlags::empty();
|
||||
|
||||
if value.contains(ClientRequestFlags::DELEGATE) {
|
||||
flags |= GssFlags::GSS_C_DELEG_FLAG;
|
||||
}
|
||||
|
||||
if value.contains(ClientRequestFlags::MUTUAL_AUTH) {
|
||||
flags |= GssFlags::GSS_C_MUTUAL_FLAG;
|
||||
}
|
||||
|
||||
if value.contains(ClientRequestFlags::REPLAY_DETECT) {
|
||||
flags |= GssFlags::GSS_C_REPLAY_FLAG;
|
||||
}
|
||||
|
||||
if value.contains(ClientRequestFlags::SEQUENCE_DETECT) {
|
||||
flags |= GssFlags::GSS_C_SEQUENCE_FLAG;
|
||||
}
|
||||
|
||||
if value.contains(ClientRequestFlags::CONFIDENTIALITY) {
|
||||
flags |= GssFlags::GSS_C_CONF_FLAG;
|
||||
}
|
||||
|
||||
if value.contains(ClientRequestFlags::INTEGRITY) {
|
||||
flags |= GssFlags::GSS_C_INTEG_FLAG;
|
||||
}
|
||||
|
||||
if value.contains(ClientRequestFlags::NO_INTEGRITY) {
|
||||
flags &= !GssFlags::GSS_C_INTEG_FLAG;
|
||||
}
|
||||
|
||||
if value.contains(ClientRequestFlags::USE_DCE_STYLE) {
|
||||
flags |= GssFlags::GSS_C_DCE_STYLE;
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthenticatorChecksumExtension {
|
||||
pub extension_type: u32,
|
||||
pub extension_value: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Encryption key.
|
||||
#[derive(Debug)]
|
||||
pub struct EncKey {
|
||||
/// Encryption type.
|
||||
pub key_type: CipherSuite,
|
||||
/// Encryption key value.
|
||||
pub key_value: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Input parameters for generating ApReq Authenticator.
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateAuthenticatorOptions<'a> {
|
||||
/// [KdcRep] from previous interaction with KDC.
|
||||
pub kdc_rep: &'a KdcRep,
|
||||
/// Sequence number.
|
||||
pub seq_num: Option<u32>,
|
||||
/// Sub-session encryption key.
|
||||
pub sub_key: Option<EncKey>,
|
||||
/// Authenticator checksum options.
|
||||
pub checksum: Option<ChecksumOptions>,
|
||||
/// Channel bindings.
|
||||
pub channel_bindings: Option<&'a ChannelBindings>,
|
||||
/// Possible authenticator extensions.
|
||||
pub extensions: Vec<AuthenticatorChecksumExtension>,
|
||||
}
|
||||
|
||||
/// Generated ApReq Authenticator.
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn generate_authenticator(options: GenerateAuthenticatorOptions<'_>) -> Result<Authenticator> {
|
||||
let GenerateAuthenticatorOptions {
|
||||
kdc_rep,
|
||||
seq_num,
|
||||
sub_key,
|
||||
checksum,
|
||||
channel_bindings,
|
||||
..
|
||||
} = options;
|
||||
|
||||
let current_date = OffsetDateTime::now_utc();
|
||||
let mut microseconds = current_date.microsecond();
|
||||
if microseconds > MAX_MICROSECONDS {
|
||||
microseconds = MAX_MICROSECONDS;
|
||||
}
|
||||
|
||||
let authorization_data = Optional::from(channel_bindings.as_ref().map(|_| {
|
||||
ExplicitContextTag8::from(AuthorizationData::from(vec![AuthorizationDataInner {
|
||||
ad_type: ExplicitContextTag0::from(IntegerAsn1::from(AD_AUTH_DATA_AP_OPTION_TYPE.to_vec())),
|
||||
ad_data: ExplicitContextTag1::from(OctetStringAsn1::from(KERB_AP_OPTIONS_CBT.to_vec())),
|
||||
}]))
|
||||
}));
|
||||
|
||||
let cksum = if let Some(ChecksumOptions {
|
||||
checksum_type,
|
||||
checksum_value,
|
||||
}) = checksum
|
||||
{
|
||||
let mut checksum_value = checksum_value.into_inner();
|
||||
if checksum_type == AUTHENTICATOR_CHECKSUM_TYPE
|
||||
&& let Some(channel_bindings) = channel_bindings
|
||||
{
|
||||
if checksum_value.len() < 20 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidParameter,
|
||||
format!(
|
||||
"Invalid authenticator checksum length: expected >= 20 but got {}. ",
|
||||
checksum_value.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
// [Authenticator Checksum](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1)
|
||||
// 4..19 - Channel binding information (19 inclusive).
|
||||
checksum_value[4..20].copy_from_slice(&compute_md5_channel_bindings_hash(channel_bindings));
|
||||
}
|
||||
Optional::from(Some(ExplicitContextTag3::from(Checksum {
|
||||
cksumtype: ExplicitContextTag0::from(IntegerAsn1::from(checksum_type)),
|
||||
checksum: ExplicitContextTag1::from(OctetStringAsn1::from(checksum_value)),
|
||||
})))
|
||||
} else {
|
||||
Optional::from(None)
|
||||
};
|
||||
|
||||
Ok(Authenticator::from(AuthenticatorInner {
|
||||
authenticator_vno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
crealm: ExplicitContextTag1::from(kdc_rep.crealm.0.clone()),
|
||||
cname: ExplicitContextTag2::from(kdc_rep.cname.0.clone()),
|
||||
cksum,
|
||||
cusec: ExplicitContextTag4::from(IntegerAsn1::from(microseconds.to_be_bytes().to_vec())),
|
||||
ctime: ExplicitContextTag5::from(KerberosTime::from(GeneralizedTime::from(current_date))),
|
||||
subkey: Optional::from(sub_key.map(|EncKey { key_type, key_value }| {
|
||||
ExplicitContextTag6::from(EncryptionKey {
|
||||
key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![key_type.into()])),
|
||||
key_value: ExplicitContextTag1::from(OctetStringAsn1::from(key_value)),
|
||||
})
|
||||
})),
|
||||
seq_number: Optional::from(seq_num.map(|seq_num| {
|
||||
ExplicitContextTag7::from(IntegerAsn1::from_bytes_be_unsigned(seq_num.to_be_bytes().to_vec()))
|
||||
})),
|
||||
authorization_data,
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all, ret)]
|
||||
pub fn generate_ap_rep(
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
seq_number: Vec<u8>,
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<ApRep> {
|
||||
let current_date = OffsetDateTime::now_utc();
|
||||
let microseconds = current_date.microsecond().min(MAX_MICROSECONDS);
|
||||
|
||||
let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
|
||||
|
||||
let enc_ap_rep_part = EncApRepPart::from(EncApRepPartInner {
|
||||
ctime: ExplicitContextTag0::from(KerberosTime::from(GeneralizedTime::from(current_date))),
|
||||
cusec: ExplicitContextTag1::from(IntegerAsn1::from(microseconds.to_be_bytes().to_vec())),
|
||||
subkey: Optional::from(None),
|
||||
seq_number: Optional::from(Some(ExplicitContextTag3::from(IntegerAsn1::from(seq_number)))),
|
||||
});
|
||||
|
||||
let cipher = encryption_type.cipher();
|
||||
|
||||
let encoded_enc_ap_rep_part = picky_asn1_der::to_vec(&enc_ap_rep_part)?;
|
||||
let encrypted_enc_ap_rep_part = cipher.encrypt(session_key.as_ref(), AP_REP_ENC, &encoded_enc_ap_rep_part)?;
|
||||
|
||||
Ok(ApRep::from(ApRepInner {
|
||||
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AP_REP_MSG_TYPE])),
|
||||
enc_part: ExplicitContextTag2::from(EncryptedData {
|
||||
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
|
||||
kvno: Optional::from(None),
|
||||
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_enc_ap_rep_part)),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn generate_tgs_ap_req(
|
||||
ticket: Ticket,
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
authenticator: &Authenticator,
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<ApReq> {
|
||||
let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
|
||||
let cipher = encryption_type.cipher();
|
||||
|
||||
let encoded_authenticator = picky_asn1_der::to_vec(&authenticator)?;
|
||||
let encrypted_authenticator = cipher.encrypt(
|
||||
session_key.as_ref(),
|
||||
TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR,
|
||||
&encoded_authenticator,
|
||||
)?;
|
||||
|
||||
trace!(
|
||||
?session_key,
|
||||
?encryption_type,
|
||||
"TGS AP_REQ authenticator encryption params",
|
||||
);
|
||||
trace!(
|
||||
plain = ?encoded_authenticator,
|
||||
encrypted = ?encrypted_authenticator,
|
||||
"TGS AP_REQ authenticator",
|
||||
);
|
||||
|
||||
Ok(ApReq::from(ApReqInner {
|
||||
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AP_REQ_MSG_TYPE])),
|
||||
ap_options: ExplicitContextTag2::from(ApOptions::from(BitString::with_bytes(vec![
|
||||
// do not need any options when ap_req uses in tgs_req pa_data
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
]))),
|
||||
ticket: ExplicitContextTag3::from(ticket),
|
||||
authenticator: ExplicitContextTag4::from(EncryptedData {
|
||||
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
|
||||
kvno: Optional::from(None),
|
||||
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_authenticator)),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn generate_ap_req(
|
||||
ticket: Ticket,
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
authenticator: &Authenticator,
|
||||
enc_params: &EncryptionParams,
|
||||
options: ApOptionsFlags,
|
||||
) -> Result<ApReq> {
|
||||
let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
|
||||
let cipher = encryption_type.cipher();
|
||||
|
||||
let encoded_authenticator = picky_asn1_der::to_vec(&authenticator)?;
|
||||
let encrypted_authenticator = cipher.encrypt(session_key.as_ref(), AP_REQ_AUTHENTICATOR, &encoded_authenticator)?;
|
||||
|
||||
trace!(
|
||||
plain = ?encoded_authenticator,
|
||||
encrypted = ?encrypted_authenticator,
|
||||
"AP_REQ authenticator",
|
||||
);
|
||||
|
||||
Ok(ApReq::from(ApReqInner {
|
||||
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AP_REQ_MSG_TYPE])),
|
||||
ap_options: ExplicitContextTag2::from(ApOptions::from(BitString::with_bytes(
|
||||
options.bits().to_be_bytes().to_vec(),
|
||||
))),
|
||||
ticket: ExplicitContextTag3::from(ticket),
|
||||
authenticator: ExplicitContextTag4::from(EncryptedData {
|
||||
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
|
||||
kvno: Optional::from(None),
|
||||
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(encrypted_authenticator)),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns supported authentication types.
|
||||
pub fn get_mech_list() -> MechTypeList {
|
||||
MechTypeList::from(vec![MechType::from(oids::ms_krb5()), MechType::from(oids::krb5())])
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", ret)]
|
||||
pub fn generate_krb_priv_request(
|
||||
ticket: Ticket,
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
new_password: &[u8],
|
||||
authenticator: &Authenticator,
|
||||
enc_params: &EncryptionParams,
|
||||
seq_num: u32,
|
||||
address: &str,
|
||||
) -> Result<KrbPrivMessage> {
|
||||
let ap_req = generate_ap_req(ticket, session_key, authenticator, enc_params, ApOptionsFlags::empty())?;
|
||||
|
||||
let enc_part = EncKrbPrivPart::from(EncKrbPrivPartInner {
|
||||
user_data: ExplicitContextTag0::from(OctetStringAsn1::from(new_password.to_vec())),
|
||||
timestamp: Optional::from(None),
|
||||
usec: Optional::from(None),
|
||||
seq_number: Optional::from(Some(ExplicitContextTag3::from(IntegerAsn1::from_bytes_be_unsigned(
|
||||
seq_num.to_be_bytes().to_vec(),
|
||||
)))),
|
||||
s_address: ExplicitContextTag4::from(HostAddress {
|
||||
addr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NET_BIOS_ADDR_TYPE])),
|
||||
address: ExplicitContextTag1::from(OctetStringAsn1::from(address.as_bytes().to_vec())),
|
||||
}),
|
||||
r_address: Optional::from(None),
|
||||
});
|
||||
|
||||
let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
|
||||
let cipher = encryption_type.cipher();
|
||||
|
||||
let encryption_key = &authenticator.0.subkey.0.as_ref().unwrap().key_value.0;
|
||||
let encoded_krb_priv = picky_asn1_der::to_vec(&enc_part)?;
|
||||
|
||||
let enc_part = cipher.encrypt(encryption_key, KRB_PRIV_ENC_PART, &encoded_krb_priv)?;
|
||||
|
||||
trace!(?encryption_key, ?encryption_type, "KRB_PRIV encryption params",);
|
||||
trace!(
|
||||
plain = ?encoded_krb_priv,
|
||||
encrypted = ?enc_part,
|
||||
"KRB_PRIV encrypted part",
|
||||
);
|
||||
|
||||
let krb_priv = KrbPriv::from(KrbPrivInner {
|
||||
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![KRB_PRIV])),
|
||||
enc_part: ExplicitContextTag3::from(EncryptedData {
|
||||
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
|
||||
kvno: Optional::from(None),
|
||||
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(enc_part)),
|
||||
}),
|
||||
});
|
||||
|
||||
Ok(KrbPrivMessage {
|
||||
ap_message: ApMessage::ApReq(ap_req),
|
||||
krb_priv,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use picky_krb::constants::types::NT_PRINCIPAL;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_set_flags() {
|
||||
let mut checksum_values = ChecksumValues::default();
|
||||
let flags = GssFlags::GSS_C_MUTUAL_FLAG | GssFlags::GSS_C_REPLAY_FLAG;
|
||||
checksum_values.set_flags(flags);
|
||||
let expected_bytes = flags.bits().to_le_bytes();
|
||||
assert_eq!(checksum_values.inner[20..24], expected_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
// ensure backwards compatibility
|
||||
let checksum_values = ChecksumValues::default();
|
||||
assert_eq!(checksum_values.into_inner(), AUTHENTICATOR_DEFAULT_CHECKSUM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flag_for_sign_and_seal() {
|
||||
let mut checksum_values = ChecksumValues::default();
|
||||
let flags = GssFlags::GSS_C_MUTUAL_FLAG
|
||||
| GssFlags::GSS_C_REPLAY_FLAG
|
||||
| GssFlags::GSS_C_SEQUENCE_FLAG
|
||||
| GssFlags::GSS_C_CONF_FLAG
|
||||
| GssFlags::GSS_C_INTEG_FLAG;
|
||||
checksum_values.set_flags(flags);
|
||||
let expected_bytes = [0x3E, 0x00, 0x00, 0x00];
|
||||
assert_eq!(checksum_values.inner[20..24], expected_bytes);
|
||||
}
|
||||
|
||||
fn cname_components(username: &str) -> Vec<String> {
|
||||
let body = generate_as_req_kdc_body(&GenerateAsReqOptions {
|
||||
realm: "CRABKA.TEST",
|
||||
username,
|
||||
cname_type: NT_PRINCIPAL,
|
||||
snames: &["krbtgt", "CRABKA.TEST"],
|
||||
nonce: &[0, 0, 0, 1],
|
||||
hostname: "host",
|
||||
context_requirements: ClientRequestFlags::empty(),
|
||||
})
|
||||
.expect("generate as-req body");
|
||||
|
||||
body.cname
|
||||
.0
|
||||
.expect("cname present")
|
||||
.0
|
||||
.name_string
|
||||
.0
|
||||
.0
|
||||
.iter()
|
||||
.map(|s| s.0.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_component_cname() {
|
||||
assert_eq!(cname_components("alice"), vec!["alice".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_principal_cname_is_split_on_slash() {
|
||||
// A `kafka/host` service principal must be encoded as two name-string
|
||||
// components, or MIT KDC rejects the principal as unknown.
|
||||
assert_eq!(
|
||||
cname_components("kafka/host"),
|
||||
vec!["kafka".to_string(), "host".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keytab_pa_datas_without_pre_auth_is_just_pac_request() {
|
||||
let pa_datas = generate_pa_datas_for_as_req_with_key(&GenerateKeytabPaDataOptions {
|
||||
key: vec![0u8; 32].into(),
|
||||
key_enctype: CipherSuite::Aes256CtsHmacSha196,
|
||||
with_pre_auth: false,
|
||||
})
|
||||
.expect("generate keytab pa-datas");
|
||||
// No pre-auth requested: only the PA-PAC-REQUEST is emitted.
|
||||
assert_eq!(pa_datas.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keytab_pa_datas_with_pre_auth_includes_enc_timestamp() {
|
||||
let pa_datas = generate_pa_datas_for_as_req_with_key(&GenerateKeytabPaDataOptions {
|
||||
key: vec![0u8; 32].into(),
|
||||
key_enctype: CipherSuite::Aes256CtsHmacSha196,
|
||||
with_pre_auth: true,
|
||||
})
|
||||
.expect("generate keytab pa-datas");
|
||||
// PA-ENC-TIMESTAMP plus PA-PAC-REQUEST.
|
||||
assert_eq!(pa_datas.len(), 2);
|
||||
}
|
||||
}
|
||||
+597
@@ -0,0 +1,597 @@
|
||||
mod as_exchange;
|
||||
mod change_password;
|
||||
pub mod extractors;
|
||||
pub mod generators;
|
||||
pub mod principal;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
pub(crate) use as_exchange::as_exchange;
|
||||
pub use change_password::change_password;
|
||||
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 rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::{Rng as _, SeedableRng as _};
|
||||
|
||||
use self::extractors::{
|
||||
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,
|
||||
};
|
||||
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, generate_tgs_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::utils::serialize_message;
|
||||
use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EC, TGT_SERVICE_NAME};
|
||||
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,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// A referral TGT has an `sname` of the form `krbtgt/<NEXT_REALM>` (RFC 4120 §3.3.3.2). In that
|
||||
/// case this returns `Some(next_realm)` and the caller must re-issue the TGS-REQ to `<NEXT_REALM>`.
|
||||
/// For an actual service ticket (e.g. `TERMSRV/host`) it returns `None`.
|
||||
fn referral_target_realm(sname: &PrincipalName) -> Option<String> {
|
||||
let names = &sname.name_string.0.0;
|
||||
|
||||
match names.as_slice() {
|
||||
[service, realm] if service.to_string().eq_ignore_ascii_case(TGT_SERVICE_NAME) => Some(realm.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs one authentication step.
|
||||
///
|
||||
/// The user should call this function until it returns `SecurityStatus::Ok`.
|
||||
pub async fn initialize_security_context<'a>(
|
||||
client: &'a mut Kerberos,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
builder: &'a mut crate::builders::FilledInitializeSecurityContext<
|
||||
'_,
|
||||
'_,
|
||||
<Kerberos as SspiImpl>::CredentialsHandle,
|
||||
>,
|
||||
) -> Result<InitializeSecurityContextResult> {
|
||||
trace!(?builder);
|
||||
|
||||
if let KerberosState::TgtExchange = client.state {
|
||||
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",
|
||||
)
|
||||
})?)?;
|
||||
|
||||
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::Preauthentication;
|
||||
|
||||
trace!(output_buffers = ?builder.output);
|
||||
|
||||
return Ok(InitializeSecurityContextResult {
|
||||
status: SecurityStatus::ContinueNeeded,
|
||||
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],
|
||||
// 4 = size of u32
|
||||
nonce: &rand.next_u32().to_be_bytes(),
|
||||
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: rand.next_u32().to_be_bytes(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
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(encryption_type as usize)?;
|
||||
|
||||
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/<NEXT_REALM>`, and the client must
|
||||
// re-issue the TGS-REQ for the same service to `<NEXT_REALM>`'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_<REALM>` (env) / krb5.conf / DNS SRV rather than the pinned
|
||||
// home-realm KDC, which cannot decrypt a `krbtgt/<NEXT_REALM>` 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 mut authenticator = generate_authenticator(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(),
|
||||
})?;
|
||||
|
||||
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?;
|
||||
|
||||
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 tgs_rep: KrbResult<TgsRep> = 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/<NEXT_REALM>`.
|
||||
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 authenticator = generate_authenticator(authenticator_options)?;
|
||||
|
||||
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 sub_session_key =
|
||||
extract_sub_session_key_from_ap_rep(&ap_rep, session_key, &client.encryption_params)?;
|
||||
let seq_number = extract_seq_number_from_ap_rep(&ap_rep, session_key, &client.encryption_params)?;
|
||||
|
||||
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, &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::<ApRep>(&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 sub_session_key =
|
||||
extract_sub_session_key_from_ap_rep(&ap_rep, session_key, &client.encryption_params)?;
|
||||
|
||||
client.encryption_params.sub_session_key = Some(sub_session_key);
|
||||
|
||||
client.next_seq_number();
|
||||
}
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use picky_asn1::restricted_string::IA5String;
|
||||
use picky_asn1::wrapper::{Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, IntegerAsn1};
|
||||
use picky_krb::constants::types::{NT_PRINCIPAL, NT_SRV_INST};
|
||||
use picky_krb::data_types::{KerberosStringAsn1, PrincipalName};
|
||||
|
||||
use super::referral_target_realm;
|
||||
|
||||
fn principal_name(name_type: u8, names: &[&str]) -> PrincipalName {
|
||||
PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![name_type])),
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(
|
||||
names
|
||||
.iter()
|
||||
.map(|n| KerberosStringAsn1::from(IA5String::from_string((*n).to_owned()).unwrap()))
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_target_realm_detects_cross_realm_tgt() {
|
||||
// krbtgt/<NEXT_REALM> => chase into NEXT_REALM.
|
||||
let sname = principal_name(NT_SRV_INST, &["krbtgt", "DEV.RJM.LOCAL"]);
|
||||
assert_eq!(referral_target_realm(&sname), Some("DEV.RJM.LOCAL".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_target_realm_is_case_insensitive_on_service() {
|
||||
// The service component comparison must ignore case ("krbtgt" vs "KRBTGT").
|
||||
let sname = principal_name(NT_SRV_INST, &["KrbTgt", "CORP.EXAMPLE.COM"]);
|
||||
assert_eq!(referral_target_realm(&sname), Some("CORP.EXAMPLE.COM".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_target_realm_ignores_actual_service_ticket() {
|
||||
// A real service ticket (TERMSRV/host) is not a referral.
|
||||
let sname = principal_name(NT_SRV_INST, &["TERMSRV", "WIN-UE7FOENEK0D.dev.rjm.local"]);
|
||||
assert_eq!(referral_target_realm(&sname), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_target_realm_ignores_non_two_component_names() {
|
||||
// A lone "krbtgt" (one component) or a 3-component name is not a referral.
|
||||
assert_eq!(referral_target_realm(&principal_name(NT_PRINCIPAL, &["krbtgt"])), None);
|
||||
assert_eq!(
|
||||
referral_target_realm(&principal_name(NT_SRV_INST, &["krbtgt", "A.COM", "B.COM"])),
|
||||
None
|
||||
);
|
||||
assert_eq!(referral_target_realm(&principal_name(NT_SRV_INST, &[])), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_target_realm_ignores_non_krbtgt_two_component_service() {
|
||||
// Two components but not krbtgt (e.g. host-based service with an instance) is not a referral.
|
||||
let sname = principal_name(NT_SRV_INST, &["cifs", "fileserver.dev.rjm.local"]);
|
||||
assert_eq!(referral_target_realm(&sname), None);
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
//! Helpers deriving the Kerberos client principal (name, name type and realm) from credentials.
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
use picky_krb::constants::types::{NT_ENTERPRISE, NT_PRINCIPAL};
|
||||
|
||||
use crate::krb::Krb5Conf;
|
||||
use crate::{Username, UsernameParts};
|
||||
|
||||
/// [MS-KILE] 3.3.5.6.1 Client Principal Lookup
|
||||
/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/6435d3fb-8cf6-4df5-a156-1277690ed59c
|
||||
//
|
||||
// FIXME: this should take a `&Username` instead of two `&str` parameters. It currently sniffs for an
|
||||
// `@` to guess the name type (and ignores `_domain` entirely), which is exactly the lossy heuristic
|
||||
// `Username`/`UsernameParts` exist to replace: the name type can be read off the user name format
|
||||
// directly (see `get_client_principal_name`). Once the deprecated string-based callers are gone (#708),
|
||||
// fold this into `get_client_principal_name`.
|
||||
pub fn get_client_principal_name_type(username: &str, _domain: &str) -> u8 {
|
||||
if username.contains('@') {
|
||||
NT_ENTERPRISE
|
||||
} else {
|
||||
NT_PRINCIPAL
|
||||
}
|
||||
}
|
||||
|
||||
/// The Kerberos client name (cname) derived from a [`Username`], along with the pieces needed to
|
||||
/// build the AS-REQ.
|
||||
///
|
||||
/// This centralizes the mapping from a [`UserNameFormat`](crate::UserNameFormat) to a Kerberos
|
||||
/// principal name type: it reads the name type off the username format explicitly instead of
|
||||
/// sniffing for an `@`, so a UPN principal is an NT-ENTERPRISE name (whose full value is the client
|
||||
/// name) and a down-level logon name is an NT-PRINCIPAL name identified by its account name alone.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct ClientPrincipalName<'a> {
|
||||
/// The name to place in the AS-REQ as the Kerberos client name (cname).
|
||||
pub name: &'a str,
|
||||
/// The domain used to resolve the client realm (the UPN suffix or the NetBIOS domain).
|
||||
pub realm_domain: &'a str,
|
||||
/// The Kerberos principal name type (`NT-ENTERPRISE` or `NT-PRINCIPAL`).
|
||||
pub name_type: u8,
|
||||
}
|
||||
|
||||
/// Derives the [`ClientPrincipalName`] for a principal from its user name format.
|
||||
pub fn get_client_principal_name(username: &Username) -> ClientPrincipalName<'_> {
|
||||
match username.parts() {
|
||||
UsernameParts::UserPrincipalName(parts) => ClientPrincipalName {
|
||||
name: parts.upn(),
|
||||
realm_domain: parts.suffix(),
|
||||
name_type: NT_ENTERPRISE,
|
||||
},
|
||||
UsernameParts::DownLevelLogonName(parts) => ClientPrincipalName {
|
||||
name: parts.account_name(),
|
||||
realm_domain: parts.netbios_domain().unwrap_or_default(),
|
||||
name_type: NT_PRINCIPAL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: like `get_client_principal_name_type`, this should take a `&Username` (or a `UsernameParts`)
|
||||
// instead of two `&str` parameters. `get_client_principal_realm_impl` re-derives the suffix by
|
||||
// splitting on `@` to reconcile the `username`/`domain` pair, which duplicates the parsing
|
||||
// `Username::parts()` already does. Migrating callers off the raw-string form (#708) would
|
||||
// let this take the parsed username directly and drop the ad-hoc split.
|
||||
pub fn get_client_principal_realm(username: &str, domain: &str) -> String {
|
||||
// https://web.mit.edu/kerberos/krb5-current/doc/user/user_config/kerberos.html#environment-variables
|
||||
|
||||
let krb5_config = env::var("KRB5_CONFIG").unwrap_or_else(|_| "/etc/krb5.conf:/usr/local/etc/krb5.conf".to_string());
|
||||
let krb5_conf_paths = krb5_config.split(':').map(Path::new).collect::<Vec<&Path>>();
|
||||
|
||||
get_client_principal_realm_impl(&krb5_conf_paths, username, domain)
|
||||
}
|
||||
|
||||
fn get_client_principal_realm_impl(krb5_conf_paths: &[&Path], username: &str, domain: &str) -> String {
|
||||
let domain = if domain.is_empty() {
|
||||
// Match `Username::parse`, which splits a UPN on its *last* `@` (account names may contain `@`).
|
||||
if let Some((_left, right)) = username.rsplit_once('@') {
|
||||
right.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
domain.to_string()
|
||||
};
|
||||
|
||||
for krb5_conf_path in krb5_conf_paths {
|
||||
if !krb5_conf_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(krb5_conf) = Krb5Conf::new_from_file(krb5_conf_path)
|
||||
&& let Some(mappings) = krb5_conf.get_values_in_section(&["domain_realm"])
|
||||
{
|
||||
for (mapping_domain, realm) in mappings {
|
||||
if matches_domain(&domain, mapping_domain) {
|
||||
return realm.to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
domain.to_uppercase()
|
||||
}
|
||||
|
||||
/// Checks if the given domain matches the mapping domain (usually from krb5.conf file).
|
||||
///
|
||||
/// # Mapping rules
|
||||
///
|
||||
/// We follow the MIT KRB5 behavior: https://github.com/krb5/krb5/commit/8f5ce824012f2caab6770df464f096c38dc4cb2e.
|
||||
///
|
||||
/// - If the mapping domain starts with a dot (e.g., `.example.com`),
|
||||
/// it matches all hosts under the domain, but not the host with the name `example.com`.
|
||||
/// For example, "test.example.com" or "d1.example.com" will match `.example.com`, but `example.com` will not.
|
||||
/// - If the mapping domain does not start with a dot (e.g., `example.com`),
|
||||
/// it matches all hosts under the domain `example.com` (including `example.com`).
|
||||
///
|
||||
/// So, the mappings order in `krb5.conf` matters.
|
||||
fn matches_domain(domain: &str, mapping_domain: &str) -> bool {
|
||||
let domain = domain.to_lowercase();
|
||||
let mapping_domain = mapping_domain.to_lowercase();
|
||||
|
||||
if mapping_domain.starts_with('.') {
|
||||
// If mapping_domain starts with a dot, it matches subdomains only
|
||||
// e.g., `.example.com` matches `test.example.com` but not `example.com`
|
||||
domain.ends_with(&mapping_domain)
|
||||
} else {
|
||||
// If mapping_domain doesn't start with a dot, it matches the domain itself
|
||||
// and all subdomains (e.g., `example.com` matches `example.com` and `test.example.com`).
|
||||
domain == mapping_domain || domain.ends_with(&format!(".{mapping_domain}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const KRB5_CONFIG_FILE_PATH: &str = "test_assets/krb5.conf";
|
||||
|
||||
#[test]
|
||||
fn client_principal_name_from_upn_is_enterprise() {
|
||||
let username = Username::parse("user@example.com").expect("UPN");
|
||||
let cname = get_client_principal_name(&username);
|
||||
assert_eq!(cname.name, "user@example.com");
|
||||
assert_eq!(cname.realm_domain, "example.com");
|
||||
assert_eq!(cname.name_type, NT_ENTERPRISE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_principal_name_from_down_level_is_principal() {
|
||||
let username = Username::parse("EXAMPLE\\user").expect("down-level logon name");
|
||||
let cname = get_client_principal_name(&username);
|
||||
assert_eq!(cname.name, "user");
|
||||
assert_eq!(cname.realm_domain, "EXAMPLE");
|
||||
assert_eq!(cname.name_type, NT_PRINCIPAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_principal_name_from_bare_name_has_empty_realm_domain() {
|
||||
let username = Username::parse("user").expect("bare name");
|
||||
let cname = get_client_principal_name(&username);
|
||||
assert_eq!(cname.name, "user");
|
||||
assert_eq!(cname.realm_domain, "");
|
||||
assert_eq!(cname.name_type, NT_PRINCIPAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_client_principal_realm_from_domain() {
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "", "TBT.COM");
|
||||
assert_eq!(realm, "TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "", "C1.DEV.TBT.COM");
|
||||
assert_eq!(realm, "TEST.TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "", "P1.C2.DEV.TBT.COM");
|
||||
assert_eq!(realm, "TEST.TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "", "DEV.TBT.COM");
|
||||
assert_eq!(realm, "DEV.TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "", "TEST.TBT.COM");
|
||||
assert_eq!(realm, "STAGE.TBT.COM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_client_principal_realm_from_username() {
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "user@tbt.com", "");
|
||||
assert_eq!(realm, "TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "user@c1.dev.tbt.com", "");
|
||||
assert_eq!(realm, "TEST.TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "user@p1.c2.dev.tbt.com", "");
|
||||
assert_eq!(realm, "TEST.TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "user@dev.tbt.com", "");
|
||||
assert_eq!(realm, "DEV.TBT.COM");
|
||||
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "user@test.tbt.com", "");
|
||||
assert_eq!(realm, "STAGE.TBT.COM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realm_from_username_splits_on_last_at() {
|
||||
// `Username::parse` splits a UPN on its *last* `@`, so the realm must be derived from the
|
||||
// suffix after the final `@` even when the account name itself contains an `@`.
|
||||
let realm = get_client_principal_realm_impl(&[Path::new(KRB5_CONFIG_FILE_PATH)], "user@dept@tbt.com", "");
|
||||
assert_eq!(realm, "TBT.COM");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user