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");
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::kdc::detect_kdc_url;
|
||||
use crate::kerberos::ServerProperties;
|
||||
use crate::negotiate::{NegotiatedProtocol, ProtocolConfig};
|
||||
use crate::{Kerberos, Result};
|
||||
|
||||
/// Kerberos client configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KerberosConfig {
|
||||
/// KDC URL
|
||||
///
|
||||
/// Depending on the scheme it is expected to be either:
|
||||
/// - a (Kerberos) KDC address (e.g.: tcp://domain:88, udp://domain:88), or
|
||||
/// - a KDC _Proxy_ URL (e.g.: <https://gateway.devolutions.net/jet/KdcProxy?token=…>)
|
||||
///
|
||||
/// That is, when the scheme is `http` or `https`, the KDC Proxy Protocol ([KKDCP]) will be
|
||||
/// used on top of the Kerberos protocol, wrapping the messages.
|
||||
/// Otherwise, the scheme must be either `tcp` or `udp`, and the KDC protocol will be used
|
||||
/// in order to communicate with the KDC server directly.
|
||||
///
|
||||
/// [KKDCP]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kkdcp/5bcebb8d-b747-4ee5-9453-428aec1c5c38
|
||||
pub kdc_url: Option<Url>,
|
||||
/// Computer name, or "workstation name", of the client machine performing the authentication attempt
|
||||
///
|
||||
/// This is also referred to as the "Source Workstation", i.e.: the name of the computer attempting to logon.
|
||||
pub client_computer_name: String,
|
||||
}
|
||||
|
||||
impl ProtocolConfig for KerberosConfig {
|
||||
fn new_instance(&self) -> Result<NegotiatedProtocol> {
|
||||
Ok(NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config(
|
||||
self.clone(),
|
||||
)?))
|
||||
}
|
||||
|
||||
fn box_clone(&self) -> Box<dyn ProtocolConfig> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_kdc_url(kdc_url: &str) -> Option<Url> {
|
||||
if !kdc_url.contains("://") {
|
||||
Url::from_str(&format!("tcp://{kdc_url}")).ok()
|
||||
} else {
|
||||
Url::from_str(kdc_url).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl KerberosConfig {
|
||||
pub fn new(kdc_url: &str, client_computer_name: String) -> Self {
|
||||
let kdc_url = parse_kdc_url(kdc_url);
|
||||
|
||||
Self {
|
||||
kdc_url,
|
||||
client_computer_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kdc_url(self, domain: &str) -> Option<Url> {
|
||||
if let Some(kdc_url) = self.kdc_url {
|
||||
Some(kdc_url)
|
||||
} else {
|
||||
detect_kdc_url(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kerberos server configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KerberosServerConfig {
|
||||
/// General Kerberos configuration.
|
||||
pub kerberos_config: KerberosConfig,
|
||||
/// Kerberos server specific parameters.
|
||||
pub server_properties: ServerProperties,
|
||||
}
|
||||
|
||||
impl ProtocolConfig for KerberosServerConfig {
|
||||
fn new_instance(&self) -> Result<NegotiatedProtocol> {
|
||||
Ok(NegotiatedProtocol::Kerberos(Kerberos::new_server_from_config(
|
||||
self.kerberos_config.clone(),
|
||||
self.server_properties.clone(),
|
||||
)?))
|
||||
}
|
||||
|
||||
fn box_clone(&self) -> Box<dyn ProtocolConfig> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use picky_krb::constants::key_usages::{ACCEPTOR_SEAL, INITIATOR_SEAL};
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
use picky_krb::crypto::aes::AesSize;
|
||||
|
||||
use crate::Secret;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncryptionParams {
|
||||
pub encryption_type: Option<CipherSuite>,
|
||||
pub session_key: Option<Secret<Vec<u8>>>,
|
||||
pub sub_session_key: Option<Secret<Vec<u8>>>,
|
||||
pub sspi_encrypt_key_usage: i32,
|
||||
pub sspi_decrypt_key_usage: i32,
|
||||
/// EC field of the Kerberos Wrap token.
|
||||
///
|
||||
/// Related documentation:
|
||||
/// * [RFC 4121: EC Field](https://www.rfc-editor.org/rfc/rfc4121#section-4.2.3).
|
||||
/// * [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550).
|
||||
///
|
||||
/// This value is different during RDP and RPC authentication.
|
||||
/// We negotiate it during the authentication process.
|
||||
pub ec: u16,
|
||||
}
|
||||
|
||||
impl EncryptionParams {
|
||||
pub fn default_for_client() -> Self {
|
||||
Self {
|
||||
encryption_type: None,
|
||||
session_key: None,
|
||||
sub_session_key: None,
|
||||
sspi_encrypt_key_usage: INITIATOR_SEAL,
|
||||
sspi_decrypt_key_usage: ACCEPTOR_SEAL,
|
||||
ec: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_for_server() -> Self {
|
||||
Self {
|
||||
encryption_type: None,
|
||||
session_key: None,
|
||||
sub_session_key: None,
|
||||
sspi_encrypt_key_usage: ACCEPTOR_SEAL,
|
||||
sspi_decrypt_key_usage: INITIATOR_SEAL,
|
||||
ec: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aes_size(&self) -> Option<AesSize> {
|
||||
self.encryption_type.as_ref().and_then(|e_type| match e_type {
|
||||
CipherSuite::Aes256CtsHmacSha196 => Some(AesSize::Aes256),
|
||||
CipherSuite::Aes128CtsHmacSha196 => Some(AesSize::Aes128),
|
||||
CipherSuite::Des3CbcSha1Kd => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
use bitflags::bitflags;
|
||||
|
||||
use crate::ClientRequestFlags;
|
||||
|
||||
bitflags! {
|
||||
/// This flags appears in the KRB_AS_REQ and KRB_TGS_REQ requests to
|
||||
/// the KDC and indicates the flags that the client wants set on the tickets.
|
||||
///
|
||||
/// [KDCOptions](https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct KdcOptions: u32 {
|
||||
const FORWARDABLE = 0x40000000;
|
||||
const FORWARDED = 0x20000000;
|
||||
const PROXIABLE = 0b10000000;
|
||||
const PROXY = 0x08000000;
|
||||
const ALLOW_POSTDATE = 0x04000000;
|
||||
const POSTDATED = 0x02000000;
|
||||
const RENEWABLE = 0x00800000;
|
||||
const OPT_HARDWARE_AUTH = 0x00100000;
|
||||
const CANONICALIZE = 0x00010000;
|
||||
const DISABLE_TRANSITED_CHECK = 0x00000020;
|
||||
const RENEWABLE_OK = 0x00000010;
|
||||
const ENC_TKT_IN_SKEY = 0x00000008;
|
||||
const RENEW = 0x00000002;
|
||||
const VALIDATE = 0x00000001;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// This flags appears in the application request (KRB_AP_REQ) and
|
||||
/// affects the way the request is processed.
|
||||
///
|
||||
/// [APOptions](https://www.rfc-editor.org/rfc/rfc4120#section-5.5.1)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ApOptions: u32 {
|
||||
const USE_SESSION_KEY = 0x40000000;
|
||||
const MUTUAL_REQUIRED = 0x20000000;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientRequestFlags> for ApOptions {
|
||||
fn from(flags: ClientRequestFlags) -> Self {
|
||||
let mut ap_options = ApOptions::empty();
|
||||
|
||||
if flags.contains(ClientRequestFlags::MUTUAL_AUTH) {
|
||||
ap_options |= ApOptions::MUTUAL_REQUIRED;
|
||||
}
|
||||
if flags.contains(ClientRequestFlags::USE_SESSION_KEY) {
|
||||
ap_options |= ApOptions::USE_SESSION_KEY;
|
||||
}
|
||||
|
||||
ap_options
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//! Functions for handling Kerberos messages on both client and server sides.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use oid::ObjectIdentifier;
|
||||
use picky_asn1::wrapper::ObjectIdentifierAsn1;
|
||||
use picky_krb::gss_api::{ApplicationTag0, KrbMessage};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::{Error, ErrorKind, Result};
|
||||
|
||||
/// Decodes incoming [KrbMessage] message and extracts [ApReq] Kerberos message.
|
||||
pub(super) fn decode_krb_message<T: DeserializeOwned>(data: &[u8], token_id: [u8; 2]) -> Result<T> {
|
||||
let krb_message = KrbMessage::<T>::decode_application_krb_message(data)?.0;
|
||||
|
||||
if krb_message.krb5_token_id != token_id {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!(
|
||||
"invalid kerberos token id: expected {:?} but got {:?}",
|
||||
token_id, krb_message.krb5_token_id
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(krb_message.krb_msg)
|
||||
}
|
||||
|
||||
pub(super) fn generate_krb_message<T: Serialize + Debug + PartialEq>(
|
||||
mech_id: ObjectIdentifier,
|
||||
krb5_token_id: [u8; 2],
|
||||
krb_msg: T,
|
||||
) -> Result<Vec<u8>> {
|
||||
let krb_blob = ApplicationTag0(KrbMessage {
|
||||
krb5_oid: ObjectIdentifierAsn1::from(mech_id),
|
||||
krb5_token_id,
|
||||
krb_msg,
|
||||
});
|
||||
|
||||
Ok(picky_asn1_der::to_vec(&krb_blob)?)
|
||||
}
|
||||
Vendored
+876
@@ -0,0 +1,876 @@
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
mod encryption_params;
|
||||
pub mod flags;
|
||||
mod messages;
|
||||
mod pa_datas;
|
||||
pub mod server;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod utils;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use picky_asn1::restricted_string::IA5String;
|
||||
use picky_asn1::wrapper::{ExplicitContextTag0, ExplicitContextTag1, OctetStringAsn1, Optional};
|
||||
use picky_krb::crypto::aes::{AesSize, checksum_sha_aes};
|
||||
use picky_krb::crypto::{CipherSuite, DecryptWithoutChecksum, EncryptWithoutChecksum};
|
||||
use picky_krb::data_types::KerberosStringAsn1;
|
||||
use picky_krb::gss_api::WrapToken;
|
||||
use picky_krb::messages::KdcProxyMessage;
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::{Rng as _, SeedableRng as _};
|
||||
use url::Url;
|
||||
|
||||
pub use self::client::initialize_security_context;
|
||||
use self::config::KerberosConfig;
|
||||
pub use self::encryption_params::EncryptionParams;
|
||||
pub use self::server::{ServerProperties, accept_security_context};
|
||||
use super::channel_bindings::ChannelBindings;
|
||||
use crate::builders::ChangePassword;
|
||||
use crate::generator::{
|
||||
GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, NetworkRequest,
|
||||
YieldPointLocal,
|
||||
};
|
||||
use crate::network_client::NetworkProtocol;
|
||||
#[cfg(feature = "scard")]
|
||||
use crate::pk_init::DhParameters;
|
||||
use crate::utils::{extract_encrypted_data, get_encryption_key, save_decrypted_data};
|
||||
use crate::{
|
||||
AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, BufferType, ContextNames, ContextSizes,
|
||||
CredentialUse, Credentials, CredentialsBuffers, DecryptionFlags, Error, ErrorKind, PACKAGE_ID_NONE,
|
||||
PackageCapabilities, PackageInfo, Result, SecurityBuffer, SecurityBufferFlags, SecurityBufferRef,
|
||||
SecurityPackageType, SecurityStatus, SessionKeys, Sspi, SspiEx, SspiImpl, detect_kdc_url,
|
||||
};
|
||||
|
||||
pub const PKG_NAME: &str = "Kerberos";
|
||||
pub const KERBEROS_VERSION: u8 = 0x05;
|
||||
pub const TGT_SERVICE_NAME: &str = "krbtgt";
|
||||
pub const KADMIN: &str = "kadmin";
|
||||
pub const CHANGE_PASSWORD_SERVICE_NAME: &str = "changepw";
|
||||
|
||||
// pub const SSPI_KDC_URL_ENV: &str = "SSPI_KDC_URL";
|
||||
pub const DEFAULT_ENCRYPTION_TYPE: CipherSuite = CipherSuite::Aes256CtsHmacSha196;
|
||||
|
||||
/// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550)
|
||||
/// The RRC field is 12 if no encryption is requested or 28 if encryption is requested
|
||||
pub const RRC: u16 = 28;
|
||||
// wrap token header len
|
||||
pub const MAX_SIGNATURE: usize = 16;
|
||||
/// Required `TOKEN` buffer length during data encryption (`encrypt_message` method call).
|
||||
///
|
||||
/// **Note**: Actual security trailer len is `SECURITY_TRAILER` + `EC`. The `EC` field is negotiated
|
||||
// during the authentication process.
|
||||
pub const SECURITY_TRAILER: usize = 60;
|
||||
|
||||
/// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550)
|
||||
///
|
||||
/// The extra count (EC) must not be zero. The sender should set extra count (EC) to 1 block - 16 bytes.
|
||||
pub(crate) const EC: u16 = 16;
|
||||
|
||||
pub static PACKAGE_INFO: LazyLock<PackageInfo> = LazyLock::new(|| PackageInfo {
|
||||
capabilities: PackageCapabilities::empty(),
|
||||
rpc_id: PACKAGE_ID_NONE,
|
||||
max_token_len: 0xbb80, // 48 000 bytes: default maximum token len in Windows
|
||||
name: SecurityPackageType::Kerberos,
|
||||
comment: String::from("Kerberos Security Package"),
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum KerberosState {
|
||||
TgtExchange,
|
||||
Preauthentication,
|
||||
ApExchange,
|
||||
Final,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Kerberos {
|
||||
pub(crate) state: KerberosState,
|
||||
pub(crate) config: KerberosConfig,
|
||||
pub(crate) auth_identity: Option<CredentialsBuffers>,
|
||||
pub(crate) encryption_params: EncryptionParams,
|
||||
pub(crate) seq_number: u32,
|
||||
pub(crate) realm: Option<String>,
|
||||
pub(crate) kdc_url: Option<Url>,
|
||||
pub(crate) channel_bindings: Option<ChannelBindings>,
|
||||
#[cfg(feature = "scard")]
|
||||
pub(crate) dh_parameters: Option<DhParameters>,
|
||||
pub(crate) krb5_user_to_user: bool,
|
||||
pub(crate) server: Option<Box<ServerProperties>>,
|
||||
}
|
||||
|
||||
impl Kerberos {
|
||||
pub fn new_client_from_config(config: KerberosConfig) -> Result<Self> {
|
||||
let kdc_url = config.kdc_url.clone();
|
||||
let mut rand = StdRng::try_from_rng(&mut SysRng)?;
|
||||
|
||||
Ok(Self {
|
||||
state: KerberosState::TgtExchange,
|
||||
config,
|
||||
auth_identity: None,
|
||||
encryption_params: EncryptionParams::default_for_client(),
|
||||
seq_number: rand.next_u32(),
|
||||
realm: None,
|
||||
kdc_url,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_server_from_config(config: KerberosConfig, server_properties: ServerProperties) -> Result<Self> {
|
||||
let kdc_url = config.kdc_url.clone();
|
||||
let mut rand = StdRng::try_from_rng(&mut SysRng)?;
|
||||
|
||||
Ok(Self {
|
||||
state: KerberosState::TgtExchange,
|
||||
config,
|
||||
auth_identity: None,
|
||||
encryption_params: EncryptionParams::default_for_server(),
|
||||
seq_number: rand.next_u32(),
|
||||
realm: None,
|
||||
kdc_url,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: Some(Box::new(server_properties)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_client(&self) -> bool {
|
||||
self.server.is_none()
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &KerberosConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub fn next_seq_number(&mut self) -> u32 {
|
||||
self.seq_number += 1;
|
||||
self.seq_number
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, skip(self))]
|
||||
pub fn get_kdc(&self) -> Option<(String, Url)> {
|
||||
let realm = self.realm.to_owned()?;
|
||||
if let Some(kdc_url) = &self.kdc_url {
|
||||
Some((realm, kdc_url.to_owned()))
|
||||
} else {
|
||||
let kdc_url = detect_kdc_url(&realm)?;
|
||||
Some((realm, kdc_url))
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&self, yield_point: &mut YieldPointLocal, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let (realm, kdc_url) = self
|
||||
.get_kdc()
|
||||
.ok_or_else(|| Error::new(ErrorKind::NoAuthenticatingAuthority, "No KDC server found"))?;
|
||||
self.send_to(yield_point, &realm, kdc_url, data).await
|
||||
}
|
||||
|
||||
/// Sends a Kerberos message to the KDC responsible for `realm`.
|
||||
///
|
||||
/// The pinned `kdc_url` (e.g. from KDC proxy settings) is authoritative only for the client's
|
||||
/// home realm. For cross-realm referrals, the target realm's KDC is resolved via
|
||||
/// [`detect_kdc_url`], so that `SSPI_KDC_URL_<REALM>` environment overrides (and the system
|
||||
/// krb5.conf / DNS SRV records) are honored for the referral hop. This lets a single auth
|
||||
/// chase a referral into a child/trusted realm without changing the pinned home-realm KDC.
|
||||
async fn send_for_realm(&self, yield_point: &mut YieldPointLocal, realm: &str, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let kdc_url = if self.realm.as_deref() == Some(realm) {
|
||||
self.kdc_url.clone().or_else(|| detect_kdc_url(realm))
|
||||
} else {
|
||||
detect_kdc_url(realm)
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::NoAuthenticatingAuthority,
|
||||
format!("No KDC server found for realm `{realm}`"),
|
||||
)
|
||||
})?;
|
||||
self.send_to(yield_point, realm, kdc_url, data).await
|
||||
}
|
||||
|
||||
async fn send_to(
|
||||
&self,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
realm: &str,
|
||||
kdc_url: Url,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
let protocol = NetworkProtocol::from_url_scheme(kdc_url.scheme()).ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidParameter,
|
||||
format!("Invalid protocol `{}` for KDC server", kdc_url.scheme()),
|
||||
)
|
||||
})?;
|
||||
|
||||
match protocol {
|
||||
NetworkProtocol::Tcp => {
|
||||
let request = NetworkRequest {
|
||||
protocol,
|
||||
url: kdc_url,
|
||||
data: data.to_vec(),
|
||||
};
|
||||
yield_point.suspend(request).await
|
||||
}
|
||||
NetworkProtocol::Udp => {
|
||||
if data.len() < 4 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InternalError,
|
||||
format!(
|
||||
"kerberos message has invalid length. expected >= 4 but got {}",
|
||||
data.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// First 4 bytes are message length and it’s not included when using UDP
|
||||
let request = NetworkRequest {
|
||||
protocol,
|
||||
url: kdc_url,
|
||||
data: data[4..].to_vec(),
|
||||
};
|
||||
yield_point.suspend(request).await
|
||||
}
|
||||
NetworkProtocol::Http | NetworkProtocol::Https => {
|
||||
let data = OctetStringAsn1::from(data.to_vec());
|
||||
let domain = KerberosStringAsn1::from(IA5String::from_string(realm.to_owned())?);
|
||||
|
||||
let kdc_proxy_message = KdcProxyMessage {
|
||||
kerb_message: ExplicitContextTag0::from(data),
|
||||
target_domain: Optional::from(Some(ExplicitContextTag1::from(domain))),
|
||||
dclocator_hint: Optional::from(None),
|
||||
};
|
||||
|
||||
let message_request = picky_asn1_der::to_vec(&kdc_proxy_message)?;
|
||||
let request = NetworkRequest {
|
||||
protocol,
|
||||
url: kdc_url,
|
||||
data: message_request,
|
||||
};
|
||||
let result_bytes = yield_point.suspend(request).await?;
|
||||
let message_response: KdcProxyMessage = picky_asn1_der::from_bytes(&result_bytes)?;
|
||||
Ok(message_response.kerb_message.0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sspi for Kerberos {
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip_all)]
|
||||
fn complete_auth_token(&mut self, _token: &mut [SecurityBuffer]) -> Result<SecurityStatus> {
|
||||
Ok(SecurityStatus::Ok)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, _flags))]
|
||||
fn encrypt_message(
|
||||
&mut self,
|
||||
_flags: crate::EncryptionFlags,
|
||||
message: &mut [SecurityBufferRef<'_>],
|
||||
) -> Result<SecurityStatus> {
|
||||
if self.state != KerberosState::Final {
|
||||
return Err(Error::new(
|
||||
ErrorKind::OutOfSequence,
|
||||
format!("Kerberos context is not established: current state: {:?}", self.state),
|
||||
));
|
||||
}
|
||||
|
||||
trace!(encryption_params = ?self.encryption_params);
|
||||
|
||||
// checks if the Token buffer present
|
||||
let _ = SecurityBufferRef::find_buffer(message, BufferType::Token)?;
|
||||
// Find `Data` buffers but skip `Data` buffers with the `READONLY_WITH_CHECKSUM`/`READONLY` flag.
|
||||
let data_to_encrypt =
|
||||
SecurityBufferRef::buffers_of_type_and_flags(message, BufferType::Data, SecurityBufferFlags::NONE);
|
||||
|
||||
let cipher = self
|
||||
.encryption_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher();
|
||||
|
||||
let seq_number = self.next_seq_number();
|
||||
|
||||
let key = get_encryption_key(&self.encryption_params)?;
|
||||
|
||||
let key_usage = self.encryption_params.sspi_encrypt_key_usage;
|
||||
|
||||
let mut wrap_token = WrapToken::with_seq_number(seq_number as u64);
|
||||
if self.server.is_some() {
|
||||
// [Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.2):
|
||||
//
|
||||
// The meanings of bits in this field (the least significant bit is bit 0) are as follows:
|
||||
// Bit Name Description
|
||||
// --------------------------------------------------------------
|
||||
// 0 SentByAcceptor When set, this flag indicates the sender
|
||||
// is the context acceptor. When not set,
|
||||
// it indicates the sender is the context
|
||||
// initiator.
|
||||
// When the Kerberos is used as the Kerberos server we have to set the `SentByAcceptor` flag.
|
||||
wrap_token.flags |= 0x01;
|
||||
}
|
||||
wrap_token.ec = self.encryption_params.ec;
|
||||
|
||||
let mut payload = data_to_encrypt.fold(Vec::new(), |mut acc, buffer| {
|
||||
acc.extend_from_slice(buffer.data());
|
||||
acc
|
||||
});
|
||||
// Add filler bytes to payload vector.
|
||||
// More info:
|
||||
// * [4.2.3. EC Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.3):
|
||||
// In Wrap tokens with confidentiality, the EC field SHALL be used to encode the number of octets in the filler.
|
||||
// * [4.2.4. Encryption and Checksum Operations](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.4):
|
||||
// payload = plaintext-data | filler | "header"
|
||||
payload.extend_from_slice(&vec![0; usize::from(self.encryption_params.ec)]);
|
||||
payload.extend_from_slice(&wrap_token.header());
|
||||
|
||||
let EncryptWithoutChecksum {
|
||||
mut encrypted,
|
||||
confounder,
|
||||
ki: _,
|
||||
} = cipher.encrypt_no_checksum(key.as_ref(), key_usage, &payload)?;
|
||||
|
||||
// Find `Data` buffers (including `Data` buffers with the `READONLY_WITH_CHECKSUM` flag).
|
||||
let mut data_to_sign =
|
||||
SecurityBufferRef::buffers_of_type(message, BufferType::Data).fold(confounder, |mut acc, buffer| {
|
||||
acc.extend_from_slice(buffer.data());
|
||||
acc
|
||||
});
|
||||
// Add filler bytes to payload vector.
|
||||
// More info:
|
||||
// * [4.2.3. EC Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.3):
|
||||
// In Wrap tokens with confidentiality, the EC field SHALL be used to encode the number of octets in the filler.
|
||||
// * [4.2.4. Encryption and Checksum Operations](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.4):
|
||||
// payload = plaintext-data | filler | "header"
|
||||
data_to_sign.extend_from_slice(&vec![0; usize::from(self.encryption_params.ec)]);
|
||||
data_to_sign.extend_from_slice(&wrap_token.header());
|
||||
|
||||
let checksum = cipher.encryption_checksum(key.as_ref(), key_usage, &data_to_sign)?;
|
||||
|
||||
encrypted.extend_from_slice(&checksum);
|
||||
|
||||
// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550):
|
||||
// The trailing metadata H1 is rotated by RRC+EC bytes, which is different from RRC alone.
|
||||
encrypted.rotate_right(usize::from(RRC + self.encryption_params.ec));
|
||||
|
||||
wrap_token.set_rrc(RRC);
|
||||
wrap_token.set_checksum(encrypted);
|
||||
|
||||
let mut raw_wrap_token = Vec::with_capacity(wrap_token.checksum.len() + WrapToken::header_len());
|
||||
wrap_token.encode(&mut raw_wrap_token)?;
|
||||
|
||||
let security_trailer_len = self.query_context_sizes()?.security_trailer.try_into()?;
|
||||
|
||||
let (token, data) = if raw_wrap_token.len() < security_trailer_len {
|
||||
(raw_wrap_token.as_slice(), &[] as &[u8])
|
||||
} else {
|
||||
raw_wrap_token.split_at(security_trailer_len)
|
||||
};
|
||||
|
||||
let data_buffer =
|
||||
SecurityBufferRef::buffers_of_type_and_flags_mut(message, BufferType::Data, SecurityBufferFlags::NONE)
|
||||
.next()
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "no buffer was provided with type Data"))?;
|
||||
|
||||
data_buffer.write_data(data)?;
|
||||
|
||||
let token_buffer = SecurityBufferRef::find_buffer_mut(message, BufferType::Token)?;
|
||||
token_buffer.write_data(token)?;
|
||||
|
||||
Ok(SecurityStatus::Ok)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
|
||||
fn decrypt_message(&mut self, message: &mut [SecurityBufferRef<'_>]) -> Result<DecryptionFlags> {
|
||||
if self.state != KerberosState::Final {
|
||||
return Err(Error::new(
|
||||
ErrorKind::OutOfSequence,
|
||||
format!("Kerberos context is not established: current state: {:?}", self.state),
|
||||
));
|
||||
}
|
||||
|
||||
trace!(encryption_params = ?self.encryption_params);
|
||||
|
||||
let encrypted = extract_encrypted_data(message)?;
|
||||
|
||||
let cipher = self
|
||||
.encryption_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher();
|
||||
|
||||
let key = get_encryption_key(&self.encryption_params)?;
|
||||
|
||||
let key_usage = self.encryption_params.sspi_decrypt_key_usage;
|
||||
|
||||
let wrap_token = WrapToken::decode(encrypted.as_slice())?;
|
||||
// [Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.2):
|
||||
//
|
||||
// The meanings of bits in this field (the least significant bit is bit 0) are as follows:
|
||||
// Bit Name Description
|
||||
// --------------------------------------------------------------
|
||||
// 0 SentByAcceptor When set, this flag indicates the sender
|
||||
// is the context acceptor. When not set,
|
||||
// it indicates the sender is the context
|
||||
// initiator.
|
||||
let is_server = u8::from(self.server.is_some());
|
||||
// If the Kerberos acts as the Kerberos application server, then the `SentByAcceptor` flag
|
||||
// of the incoming WRAP token must be disabled (because it is sent by initiator).
|
||||
if wrap_token.flags & 0x01 == is_server {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"invalid WRAP token SentByAcceptor flag",
|
||||
));
|
||||
}
|
||||
// 1 Sealed When set in Wrap tokens, this flag
|
||||
// indicates confidentiality is provided
|
||||
// for. It SHALL NOT be set in MIC tokens.
|
||||
//
|
||||
// When the Sealed flag is clear, this is an integrity-only Wrap token
|
||||
// (GSS_Wrap with conf_req_flag == FALSE, RFC 4121 §4.2.4): the data is
|
||||
// carried in cleartext followed by the checksum, rather than encrypted.
|
||||
// Stock SASL/GSSAPI clients (e.g. Java/Kafka) use conf=false for the
|
||||
// RFC 4752 security-layer negotiation reply, so the acceptor must
|
||||
// accept it instead of demanding confidentiality.
|
||||
if wrap_token.flags & 0b10 != 0b10 {
|
||||
// The integrity-only checksum is the AES-SHA1 keyed HMAC
|
||||
// (`checksum_sha_aes`), which is only defined for the AES cipher
|
||||
// suites. Reject non-AES contexts (e.g. `Des3CbcSha1Kd`) explicitly
|
||||
// rather than silently treating them as AES-256, which would produce
|
||||
// incorrect checksum validation.
|
||||
let aes_size = self.encryption_params.aes_size().ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::UnsupportedFunction,
|
||||
"integrity-only WRAP tokens are only supported for AES cipher suites",
|
||||
)
|
||||
})?;
|
||||
return decrypt_integrity_only_wrap(&wrap_token, key.as_ref(), &aes_size, key_usage, message);
|
||||
}
|
||||
|
||||
let mut checksum = wrap_token.checksum;
|
||||
// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550):
|
||||
// The trailing metadata H1 is rotated by RRC+EC bytes, which is different from RRC alone.
|
||||
checksum.rotate_left((RRC + wrap_token.ec).into());
|
||||
|
||||
let DecryptWithoutChecksum {
|
||||
plaintext: decrypted,
|
||||
confounder,
|
||||
checksum,
|
||||
ki: _,
|
||||
} = cipher.decrypt_no_checksum(key.as_ref(), key_usage, &checksum)?;
|
||||
|
||||
if decrypted.len() < usize::from(wrap_token.ec) + WrapToken::header_len() {
|
||||
return Err(Error::new(ErrorKind::DecryptFailure, "decrypted data is too short"));
|
||||
}
|
||||
|
||||
let plaintext_len = decrypted.len() - usize::from(wrap_token.ec) - WrapToken::header_len();
|
||||
|
||||
let plaintext = &decrypted[0..plaintext_len];
|
||||
let wrap_token_header = &decrypted[plaintext_len..];
|
||||
|
||||
// Find `Data` buffers (including `Data` buffers with the `READONLY_WITH_CHECKSUM` flag).
|
||||
let mut data_to_sign =
|
||||
SecurityBufferRef::buffers_of_type(message, BufferType::Data).fold(confounder, |mut acc, buffer| {
|
||||
if buffer
|
||||
.buffer_flags()
|
||||
.contains(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM)
|
||||
{
|
||||
acc.extend_from_slice(buffer.data());
|
||||
} else {
|
||||
// The `Data` buffer contains encrypted data, but the checksum was calculated over the decrypted data.
|
||||
// So, we replace encrypted data with decrypted one.
|
||||
// Note: our implementation expect maximum one plain `DATA` buffer but multiple `DATA` buffers
|
||||
// with `SECBUFFER_READONLY_WITH_CHECKSUM` flag are allowed.
|
||||
acc.extend_from_slice(plaintext);
|
||||
}
|
||||
acc
|
||||
});
|
||||
data_to_sign.extend_from_slice(wrap_token_header);
|
||||
|
||||
let calculated_checksum = cipher.encryption_checksum(key.as_ref(), key_usage, &data_to_sign)?;
|
||||
|
||||
if calculated_checksum != checksum {
|
||||
return Err(picky_krb::crypto::KerberosCryptoError::IntegrityCheck.into());
|
||||
}
|
||||
|
||||
save_decrypted_data(plaintext, message)?;
|
||||
|
||||
Ok(DecryptionFlags::empty())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
|
||||
fn query_context_sizes(&mut self) -> Result<ContextSizes> {
|
||||
// We prevent users from calling `query_context_sizes` on a non-established security context
|
||||
// because it can lead to invalid values being returned.
|
||||
if self.state != KerberosState::Final {
|
||||
return Err(Error::new(
|
||||
ErrorKind::OutOfSequence,
|
||||
format!("Kerberos context is not established: current state: {:?}", self.state),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ContextSizes {
|
||||
max_token: PACKAGE_INFO.max_token_len,
|
||||
max_signature: MAX_SIGNATURE as u32,
|
||||
block: 0,
|
||||
security_trailer: SECURITY_TRAILER as u32 + u32::from(self.encryption_params.ec),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
|
||||
fn query_context_names(&mut self) -> Result<ContextNames> {
|
||||
if let Some(client) = self.server.as_ref().and_then(|server| server.client.as_ref()) {
|
||||
return Ok(ContextNames {
|
||||
username: client.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(CredentialsBuffers::AuthIdentity(identity_buffers)) = &self.auth_identity {
|
||||
let identity =
|
||||
AuthIdentity::try_from(identity_buffers).map_err(|e| Error::new(ErrorKind::InvalidParameter, e))?;
|
||||
|
||||
return Ok(ContextNames {
|
||||
username: identity.username,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "scard")]
|
||||
if let Some(CredentialsBuffers::SmartCard(ref identity_buffers)) = self.auth_identity {
|
||||
let username = identity_buffers.username.to_string();
|
||||
let username = crate::Username::parse(&username).map_err(|e| Error::new(ErrorKind::InvalidParameter, e))?;
|
||||
return Ok(ContextNames { username });
|
||||
}
|
||||
|
||||
Err(Error::new(
|
||||
ErrorKind::NoCredentials,
|
||||
"requested names, but no credentials were provided",
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
|
||||
fn query_context_package_info(&mut self) -> Result<PackageInfo> {
|
||||
crate::query_security_package_info(SecurityPackageType::Kerberos)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
|
||||
fn query_context_cert_trust_status(&mut self) -> Result<crate::CertTrustStatus> {
|
||||
Err(Error::new(
|
||||
ErrorKind::UnsupportedFunction,
|
||||
"certificate trust status is not supported".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", fields(state = ?self.state), skip(self))]
|
||||
fn query_context_session_key(&self) -> Result<SessionKeys> {
|
||||
Ok(SessionKeys {
|
||||
session_key: get_encryption_key(&self.encryption_params)?.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn change_password<'a>(&'a mut self, change_password: ChangePassword<'a>) -> Result<GeneratorChangePassword<'a>> {
|
||||
Ok(GeneratorChangePassword::new(move |mut yield_point| async move {
|
||||
client::change_password(self, &mut yield_point, change_password).await
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_signature(
|
||||
&mut self,
|
||||
_flags: u32,
|
||||
_message: &mut [SecurityBufferRef<'_>],
|
||||
_sequence_number: u32,
|
||||
) -> Result<()> {
|
||||
Err(Error::new(
|
||||
ErrorKind::UnsupportedFunction,
|
||||
"make_signature is not supported. use encrypt_message to sign messages instead",
|
||||
))
|
||||
}
|
||||
|
||||
fn verify_signature(&mut self, _message: &mut [SecurityBufferRef<'_>], _sequence_number: u32) -> Result<u32> {
|
||||
Err(Error::new(
|
||||
ErrorKind::UnsupportedFunction,
|
||||
"verify_signature is not supported. use decrypt_message to verify signatures instead",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies an integrity-only (GSS_Wrap conf_req_flag == FALSE) Wrap token and
|
||||
/// returns its cleartext payload in the message buffers.
|
||||
///
|
||||
/// Per RFC 4121 §4.2.4, an unsealed Wrap token carries the data in the clear
|
||||
/// followed by the checksum: `{header | plaintext | checksum}`, right-rotated
|
||||
/// by RRC. The checksum is computed over `plaintext | header`, where the EC and
|
||||
/// RRC fields of the 16-octet header are zeroed for the computation. Wrap tokens
|
||||
/// — sealed or not — use the SEAL key usage (the SIGN usage is reserved for MIC
|
||||
/// tokens), so `key_usage` is the same value the sealed path uses. The checksum
|
||||
/// itself is the keyed get_mic checksum (Kc-derived), not the Ki-derived
|
||||
/// encryption integrity hash used for the confidential path.
|
||||
fn decrypt_integrity_only_wrap(
|
||||
wrap_token: &WrapToken,
|
||||
key: &[u8],
|
||||
aes_size: &AesSize,
|
||||
key_usage: i32,
|
||||
message: &mut [SecurityBufferRef<'_>],
|
||||
) -> Result<DecryptionFlags> {
|
||||
// `WrapToken::decode` stores everything after the 16-octet header in
|
||||
// `checksum`; for an unsealed token that is `plaintext | trailing-checksum`.
|
||||
let mut data = wrap_token.checksum.clone();
|
||||
if data.is_empty() {
|
||||
return Err(Error::new(ErrorKind::DecryptFailure, "empty integrity-only WRAP token"));
|
||||
}
|
||||
|
||||
// Undo the sender's right rotation. For integrity-only tokens the rotation
|
||||
// is by RRC alone (the EC octets are checksum, not filler).
|
||||
let rotate = usize::from(wrap_token.rrc) % data.len();
|
||||
data.rotate_left(rotate);
|
||||
|
||||
let ec = usize::from(wrap_token.ec);
|
||||
if data.len() < ec {
|
||||
return Err(Error::new(
|
||||
ErrorKind::DecryptFailure,
|
||||
"integrity-only WRAP token shorter than its checksum",
|
||||
));
|
||||
}
|
||||
let plaintext_len = data.len() - ec;
|
||||
let (plaintext, received_checksum) = data.split_at(plaintext_len);
|
||||
|
||||
// Checksum input: plaintext followed by the 16-octet header with the EC and
|
||||
// RRC fields zeroed (RFC 4121 §4.2.4).
|
||||
let mut header = wrap_token.header();
|
||||
header[4..8].copy_from_slice(&[0, 0, 0, 0]);
|
||||
|
||||
let mut to_sign = plaintext.to_vec();
|
||||
to_sign.extend_from_slice(&header);
|
||||
|
||||
let calculated = checksum_sha_aes(key, key_usage, &to_sign, aes_size)?;
|
||||
|
||||
if calculated.as_slice() != received_checksum {
|
||||
return Err(picky_krb::crypto::KerberosCryptoError::IntegrityCheck.into());
|
||||
}
|
||||
|
||||
save_decrypted_data(plaintext, message)?;
|
||||
|
||||
Ok(DecryptionFlags::empty())
|
||||
}
|
||||
|
||||
impl SspiImpl for Kerberos {
|
||||
type CredentialsHandle = Option<CredentialsBuffers>;
|
||||
|
||||
type AuthenticationData = Credentials;
|
||||
|
||||
#[instrument(level = "trace", ret, fields(state = ?self.state), skip(self))]
|
||||
fn acquire_credentials_handle_impl(
|
||||
&mut self,
|
||||
builder: crate::builders::FilledAcquireCredentialsHandle<'_, Self::CredentialsHandle, Self::AuthenticationData>,
|
||||
) -> Result<AcquireCredentialsHandleResult<Self::CredentialsHandle>> {
|
||||
if builder.credential_use == CredentialUse::Outbound && builder.auth_data.is_none() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::NoCredentials,
|
||||
"the client must specify the auth data",
|
||||
));
|
||||
}
|
||||
|
||||
self.auth_identity = builder
|
||||
.auth_data
|
||||
.cloned()
|
||||
.map(|auth_data| auth_data.try_into())
|
||||
.transpose()?;
|
||||
|
||||
Ok(AcquireCredentialsHandleResult {
|
||||
credentials_handle: self.auth_identity.clone(),
|
||||
expiry: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, builder))]
|
||||
fn accept_security_context_impl<'a>(
|
||||
&'a mut self,
|
||||
builder: crate::builders::FilledAcceptSecurityContext<'a, Self::CredentialsHandle>,
|
||||
) -> Result<GeneratorAcceptSecurityContext<'a>> {
|
||||
Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move {
|
||||
self.accept_security_context_impl(&mut yield_point, builder).await
|
||||
}))
|
||||
}
|
||||
|
||||
fn initialize_security_context_impl<'ctx, 'b, 'g>(
|
||||
&'ctx mut self,
|
||||
builder: &'b mut crate::builders::FilledInitializeSecurityContext<'ctx, 'ctx, Self::CredentialsHandle>,
|
||||
) -> Result<GeneratorInitSecurityContext<'g>>
|
||||
where
|
||||
'ctx: 'b,
|
||||
'b: 'g,
|
||||
{
|
||||
Ok(GeneratorInitSecurityContext::new(move |mut yield_point| async move {
|
||||
self.initialize_security_context_impl(&mut yield_point, builder).await
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Kerberos {
|
||||
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, change_password))]
|
||||
pub async fn change_password(
|
||||
&'a mut self,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
change_password: ChangePassword<'a>,
|
||||
) -> Result<()> {
|
||||
client::change_password(self, yield_point, change_password).await
|
||||
}
|
||||
|
||||
pub(crate) async fn accept_security_context_impl(
|
||||
&'a mut self,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
builder: crate::builders::FilledAcceptSecurityContext<'a, <Self as SspiImpl>::CredentialsHandle>,
|
||||
) -> Result<AcceptSecurityContextResult> {
|
||||
accept_security_context(self, yield_point, builder).await
|
||||
}
|
||||
|
||||
pub(crate) async fn initialize_security_context_impl(
|
||||
&'a mut self,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
builder: &'a mut crate::builders::FilledInitializeSecurityContext<
|
||||
'_,
|
||||
'_,
|
||||
<Self as SspiImpl>::CredentialsHandle,
|
||||
>,
|
||||
) -> Result<crate::InitializeSecurityContextResult> {
|
||||
initialize_security_context(self, yield_point, builder).await
|
||||
}
|
||||
}
|
||||
|
||||
impl SspiEx for Kerberos {
|
||||
#[instrument(level = "trace", ret, fields(state = ?self.state), skip(self))]
|
||||
fn custom_set_auth_identity(&mut self, identity: Self::AuthenticationData) -> Result<()> {
|
||||
self.auth_identity = Some(identity.try_into()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_mic_token(&mut self, token: &[u8], data: &[u8], _: crate::private::Sealed) -> Result<()> {
|
||||
utils::validate_mic_token(self.is_client(), token, &self.encryption_params, data)
|
||||
}
|
||||
|
||||
fn generate_mic_token(&mut self, data: &[u8], _: crate::private::Sealed) -> Result<Vec<u8>> {
|
||||
// Do not increment sequence number for MIC token if security context is not established.
|
||||
// We do not want to mess up the sequence number.
|
||||
let seq_number = if self.encryption_params.sub_session_key.is_some() {
|
||||
self.next_seq_number()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let session_key = self
|
||||
.encryption_params
|
||||
.sub_session_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(ErrorKind::InternalError, "kerberos sub-session key is not set"))?;
|
||||
utils::generate_mic_token(self.is_client(), u64::from(seq_number), data.to_vec(), session_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "__test-data", test))]
|
||||
pub mod test_data {
|
||||
use std::time::Duration;
|
||||
|
||||
use picky_asn1::restricted_string::IA5String;
|
||||
use picky_asn1::wrapper::{Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, IntegerAsn1};
|
||||
use picky_krb::constants::key_usages::{ACCEPTOR_SEAL, INITIATOR_SEAL};
|
||||
use picky_krb::constants::types::NT_SRV_INST;
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
use picky_krb::data_types::{KerberosStringAsn1, PrincipalName};
|
||||
use picky_krb::gss_api::MechTypeList;
|
||||
|
||||
use super::{EncryptionParams, KerberosConfig, KerberosState};
|
||||
use crate::kerberos::ServerProperties;
|
||||
use crate::{AuthIdentityBuffers, CredentialsBuffers, Kerberos, Secret, Utf16String, ZeroizedUtf16String};
|
||||
|
||||
const SESSION_KEY: &[u8] = &[
|
||||
21, 56, 207, 133, 152, 47, 177, 117, 223, 235, 169, 237, 173, 202, 11, 254, 142, 185, 237, 5, 97, 79, 112, 46,
|
||||
73, 182, 117, 0, 35, 91, 24, 66,
|
||||
];
|
||||
const SUB_SESSION_KEY: &[u8] = &[
|
||||
146, 61, 191, 46, 26, 68, 247, 94, 124, 95, 1, 190, 15, 185, 245, 64, 18, 203, 212, 49, 43, 222, 254, 217, 85,
|
||||
222, 7, 92, 254, 153, 105, 144,
|
||||
];
|
||||
|
||||
pub fn fake_client() -> Kerberos {
|
||||
Kerberos {
|
||||
state: KerberosState::Final,
|
||||
config: KerberosConfig {
|
||||
kdc_url: None,
|
||||
client_computer_name: "hostname".into(),
|
||||
},
|
||||
auth_identity: Some(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers {
|
||||
user: Utf16String::from_str("fake_user"),
|
||||
domain: Utf16String::from_str("FAKE_DOMAIN"),
|
||||
password: Secret::new(ZeroizedUtf16String(Utf16String::from_str("fake_password"))),
|
||||
})),
|
||||
encryption_params: EncryptionParams {
|
||||
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
|
||||
session_key: Some(SESSION_KEY.to_vec().into()),
|
||||
sub_session_key: Some(SUB_SESSION_KEY.to_vec().into()),
|
||||
sspi_encrypt_key_usage: INITIATOR_SEAL,
|
||||
sspi_decrypt_key_usage: ACCEPTOR_SEAL,
|
||||
ec: 0,
|
||||
},
|
||||
seq_number: 1234,
|
||||
realm: None,
|
||||
kdc_url: None,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fake_server_properties() -> ServerProperties {
|
||||
ServerProperties {
|
||||
mech_types: MechTypeList::from(Vec::new()),
|
||||
max_time_skew: Duration::from_secs(3 * 60),
|
||||
ticket_decryption_key: None,
|
||||
service_name: PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![
|
||||
KerberosStringAsn1::from(IA5String::from_string("TERMSRV".to_owned()).unwrap()),
|
||||
KerberosStringAsn1::from(IA5String::from_string("VM1.example.com".to_owned()).unwrap()),
|
||||
])),
|
||||
},
|
||||
additional_service_keys: Vec::new(),
|
||||
user: None,
|
||||
client: None,
|
||||
authenticators_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fake_server() -> Kerberos {
|
||||
Kerberos {
|
||||
state: KerberosState::Final,
|
||||
config: KerberosConfig {
|
||||
kdc_url: None,
|
||||
client_computer_name: "hostname".into(),
|
||||
},
|
||||
auth_identity: None,
|
||||
encryption_params: EncryptionParams {
|
||||
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
|
||||
session_key: Some(SESSION_KEY.to_vec().into()),
|
||||
sub_session_key: Some(SUB_SESSION_KEY.to_vec().into()),
|
||||
sspi_encrypt_key_usage: ACCEPTOR_SEAL,
|
||||
sspi_decrypt_key_usage: INITIATOR_SEAL,
|
||||
ec: 0,
|
||||
},
|
||||
seq_number: 0,
|
||||
realm: None,
|
||||
kdc_url: None,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: Some(Box::new(fake_server_properties())),
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
use picky_krb::data_types::PaData;
|
||||
use picky_krb::messages::AsRep;
|
||||
|
||||
use crate::kerberos::client::extractors::{extract_session_key_from_as_rep, extract_session_key_from_as_rep_with_key};
|
||||
use crate::kerberos::client::generators::{
|
||||
GenerateAsPaDataOptions as AuthIdentityPaDataOptions, GenerateKeytabPaDataOptions,
|
||||
generate_pa_datas_for_as_req as generate_password_based,
|
||||
generate_pa_datas_for_as_req_with_key as generate_keytab_based,
|
||||
};
|
||||
use crate::kerberos::encryption_params::EncryptionParams;
|
||||
#[cfg(feature = "scard")]
|
||||
use crate::pk_init::{
|
||||
GenerateAsPaDataOptions as SmartCardPaDataOptions, generate_pa_datas_for_as_req as generate_private_key_based,
|
||||
};
|
||||
use crate::{Result, Secret};
|
||||
|
||||
// PA-DATAs are very different for the Kerberos logon using username+password and smart card.
|
||||
// This enum provides a unified way to generate PA-DATAs based on the provided options.
|
||||
pub(crate) enum AsReqPaDataOptions<'a> {
|
||||
AuthIdentity(AuthIdentityPaDataOptions<'a>),
|
||||
#[cfg(feature = "scard")]
|
||||
SmartCard(Box<SmartCardPaDataOptions<'a>>),
|
||||
Keytab(GenerateKeytabPaDataOptions),
|
||||
}
|
||||
|
||||
impl AsReqPaDataOptions<'_> {
|
||||
pub(crate) fn generate(&mut self) -> Result<Vec<PaData>> {
|
||||
match self {
|
||||
AsReqPaDataOptions::AuthIdentity(options) => generate_password_based(options),
|
||||
#[cfg(feature = "scard")]
|
||||
AsReqPaDataOptions::SmartCard(options) => generate_private_key_based(options),
|
||||
AsReqPaDataOptions::Keytab(options) => generate_keytab_based(options),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_pre_auth(&mut self, pre_auth: bool) {
|
||||
match self {
|
||||
AsReqPaDataOptions::AuthIdentity(options) => options.with_pre_auth = pre_auth,
|
||||
#[cfg(feature = "scard")]
|
||||
AsReqPaDataOptions::SmartCard(options) => options.with_pre_auth = pre_auth,
|
||||
AsReqPaDataOptions::Keytab(options) => options.with_pre_auth = pre_auth,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_salt(&mut self, salt: Vec<u8>) {
|
||||
match self {
|
||||
AsReqPaDataOptions::AuthIdentity(options) => options.salt = salt,
|
||||
#[cfg(feature = "scard")]
|
||||
AsReqPaDataOptions::SmartCard(_) => {}
|
||||
// The keytab key is pre-derived; the KDC-supplied salt is irrelevant.
|
||||
AsReqPaDataOptions::Keytab(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ApRep session key extraction process is different for the Kerberos logon using username+password and smart card.
|
||||
// This enum provides a unified way to extract session key from the AsRep.
|
||||
#[derive(Debug)]
|
||||
pub(super) enum AsRepSessionKeyExtractor<'a> {
|
||||
AuthIdentity {
|
||||
salt: &'a str,
|
||||
password: &'a str,
|
||||
enc_params: &'a EncryptionParams,
|
||||
},
|
||||
Keytab {
|
||||
key: &'a [u8],
|
||||
enc_params: &'a EncryptionParams,
|
||||
},
|
||||
#[cfg(feature = "scard")]
|
||||
SmartCard {
|
||||
dh_parameters: &'a mut crate::pk_init::DhParameters,
|
||||
enc_params: &'a mut EncryptionParams,
|
||||
},
|
||||
}
|
||||
|
||||
impl AsRepSessionKeyExtractor<'_> {
|
||||
#[instrument(level = "trace", ret, skip(self))]
|
||||
pub(super) fn session_key(&mut self, as_rep: &AsRep) -> Result<Secret<Vec<u8>>> {
|
||||
match self {
|
||||
AsRepSessionKeyExtractor::AuthIdentity {
|
||||
salt,
|
||||
password,
|
||||
enc_params,
|
||||
} => extract_session_key_from_as_rep(as_rep, salt, password, enc_params),
|
||||
AsRepSessionKeyExtractor::Keytab { key, enc_params } => {
|
||||
extract_session_key_from_as_rep_with_key(as_rep, key, enc_params)
|
||||
}
|
||||
#[cfg(feature = "scard")]
|
||||
AsRepSessionKeyExtractor::SmartCard {
|
||||
dh_parameters,
|
||||
enc_params,
|
||||
} => {
|
||||
use picky_asn1_x509::signed_data::SignedData;
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
use picky_krb::crypto::diffie_hellman::{DhNonce, generate_key};
|
||||
use picky_krb::pkinit::PaPkAsRep;
|
||||
|
||||
use crate::pk_init::{Wrapper, extract_server_dh_public_key};
|
||||
use crate::pku2u::{
|
||||
extract_pa_pk_as_rep, extract_server_nonce, validate_server_p2p_certificate, validate_signed_data,
|
||||
};
|
||||
use crate::{Error, ErrorKind, check_if_empty, pku2u};
|
||||
|
||||
let dh_rep_info = match extract_pa_pk_as_rep(as_rep)? {
|
||||
PaPkAsRep::DhInfo(dh) => dh.0,
|
||||
PaPkAsRep::EncKeyPack(_) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::OperationNotSupported,
|
||||
"encKeyPack is not supported for the PA-PK-AS-REP",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let server_nonce = extract_server_nonce(&dh_rep_info)?;
|
||||
dh_parameters.server_nonce = Some(server_nonce);
|
||||
|
||||
let wrapped_signed_data: Wrapper<SignedData> =
|
||||
picky_asn1_der::from_bytes(&dh_rep_info.dh_signed_data.0)?;
|
||||
let signed_data = wrapped_signed_data.content.0;
|
||||
|
||||
let rsa_public_key = validate_server_p2p_certificate(&signed_data)?;
|
||||
validate_signed_data(&signed_data, &rsa_public_key)?;
|
||||
|
||||
let public_key = extract_server_dh_public_key(&signed_data)?;
|
||||
dh_parameters.other_public_key = Some(public_key);
|
||||
|
||||
enc_params.encryption_type = Some(CipherSuite::try_from(as_rep.0.enc_part.0.etype.0.0.as_slice())?);
|
||||
|
||||
let key = generate_key(
|
||||
check_if_empty!(dh_parameters.other_public_key.as_ref(), "dh public key is not set"),
|
||||
&dh_parameters.private_key,
|
||||
&dh_parameters.modulus,
|
||||
Some(DhNonce {
|
||||
client_nonce: check_if_empty!(dh_parameters.client_nonce.as_ref(), "dh client none is not set"),
|
||||
server_nonce: check_if_empty!(
|
||||
dh_parameters.server_nonce.as_ref(),
|
||||
"dh server nonce is not set"
|
||||
),
|
||||
}),
|
||||
check_if_empty!(enc_params.encryption_type.as_ref(), "encryption type is not set")
|
||||
.cipher()
|
||||
.as_ref(),
|
||||
)?;
|
||||
|
||||
let session_key = pku2u::extract_session_key_from_as_rep(as_rep, &key, enc_params)?;
|
||||
|
||||
Ok(session_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
use picky_krb::data_types::Ticket;
|
||||
use picky_krb::messages::TgtReq;
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::{Rng as _, SeedableRng as _};
|
||||
|
||||
use crate::generator::YieldPointLocal;
|
||||
use crate::kerberos::client::extractors::extract_encryption_params_from_as_rep;
|
||||
use crate::kerberos::client::generators::{GenerateAsPaDataOptions, GenerateAsReqOptions, generate_as_req_kdc_body};
|
||||
use crate::kerberos::client::principal::{get_client_principal_name_type, get_client_principal_realm};
|
||||
use crate::kerberos::pa_datas::{AsRepSessionKeyExtractor, AsReqPaDataOptions};
|
||||
use crate::kerberos::{TGT_SERVICE_NAME, client};
|
||||
use crate::{ClientRequestFlags, CredentialsBuffers, Error, ErrorKind, Kerberos, Result};
|
||||
|
||||
/// Requests the TGT ticket from KDC.
|
||||
///
|
||||
/// Basically, it performs the AS exchange, saves the session key, and returns the ticket.
|
||||
pub(crate) async fn request_tgt(
|
||||
server: &mut Kerberos,
|
||||
credentials: &CredentialsBuffers,
|
||||
tgt_req: &TgtReq,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
) -> Result<Ticket> {
|
||||
let service_name = &server
|
||||
.server
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidHandle,
|
||||
"Kerberos server properties are not initialized",
|
||||
)
|
||||
})?
|
||||
.service_name;
|
||||
if tgt_req.server_name.0 != *service_name {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!(
|
||||
"invalid ticket service name ({:?}): Kerberos server is configured for {:?}",
|
||||
tgt_req.server_name.0, service_name
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
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(_) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::UnsupportedPreAuth,
|
||||
"smart card credentials are not supported in Kerberos application server",
|
||||
));
|
||||
}
|
||||
CredentialsBuffers::Keytab(_) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::UnsupportedPreAuth,
|
||||
"keytab credentials are not supported in Kerberos application server",
|
||||
));
|
||||
}
|
||||
};
|
||||
server.realm = Some(realm.clone());
|
||||
|
||||
let mut rand = StdRng::try_from_rng(&mut SysRng)?;
|
||||
let nonce = rand.next_u32();
|
||||
let options = GenerateAsReqOptions {
|
||||
realm: &realm,
|
||||
username: &username,
|
||||
cname_type,
|
||||
snames: &[TGT_SERVICE_NAME, &realm],
|
||||
// 4 = size of u32
|
||||
nonce: &nonce.to_be_bytes(),
|
||||
hostname: &server.config.client_computer_name,
|
||||
context_requirements: ClientRequestFlags::empty(),
|
||||
};
|
||||
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: server.encryption_params.clone(),
|
||||
with_pre_auth: false,
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "scard")]
|
||||
CredentialsBuffers::SmartCard(_) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::UnsupportedPreAuth,
|
||||
"smart card credentials are not supported in Kerberos application server",
|
||||
));
|
||||
}
|
||||
CredentialsBuffers::Keytab(_) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::UnsupportedPreAuth,
|
||||
"keytab credentials are not supported in Kerberos application server",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let as_rep = client::as_exchange(server, yield_point, &kdc_req_body, pa_data_options).await?;
|
||||
|
||||
debug!("AS exchange finished successfully.");
|
||||
|
||||
server.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)?;
|
||||
server.encryption_params.encryption_type = Some(encryption_type);
|
||||
|
||||
let mut session_key_extractor = AsRepSessionKeyExtractor::AuthIdentity {
|
||||
salt: &salt,
|
||||
password: &password,
|
||||
enc_params: &mut server.encryption_params,
|
||||
};
|
||||
|
||||
let server_props = server.server.as_mut().ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidHandle,
|
||||
"Kerberos server properties are not initialized",
|
||||
)
|
||||
})?;
|
||||
server_props.ticket_decryption_key = Some(session_key_extractor.session_key(&as_rep)?);
|
||||
|
||||
Ok(as_rep.0.ticket.0)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use picky_asn1::wrapper::GeneralizedTimeAsn1;
|
||||
use picky_krb::data_types::{Microseconds, PrincipalName};
|
||||
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub struct AuthenticatorCacheRecord {
|
||||
pub cname: PrincipalName,
|
||||
pub sname: PrincipalName,
|
||||
pub ctime: GeneralizedTimeAsn1,
|
||||
pub microseconds: Microseconds,
|
||||
}
|
||||
|
||||
// https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq
|
||||
// > When implementing both `Hash` and `Eq`, it is important that the following property holds:
|
||||
// ```
|
||||
// k1 == k2 -> hash(k1) == hash(k2)
|
||||
// ```
|
||||
// The `PrincipalName` implements the `PartialEq` trait but does not implement the `Hash` trait.
|
||||
// We implement `PartialEq` manually to make sure we follow required properties.
|
||||
impl PartialEq for AuthenticatorCacheRecord {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn compare_principal_names(name_1: &PrincipalName, name_2: &PrincipalName) -> bool {
|
||||
if name_1.name_type.0 != name_2.name_type.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let names_1 = &name_1.name_string.0.0;
|
||||
let names_2 = &name_2.name_string.0.0;
|
||||
|
||||
if names_1.len() != names_2.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name_1, name_2) in names_1.iter().zip(names_2.iter()) {
|
||||
if name_1.0 != name_2.0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
compare_principal_names(&self.cname, &other.cname)
|
||||
&& compare_principal_names(&self.sname, &other.sname)
|
||||
&& self.ctime == other.ctime
|
||||
&& self.microseconds == other.microseconds
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for AuthenticatorCacheRecord {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
fn hash_principal_name<H: Hasher>(name: &PrincipalName, state: &mut H) {
|
||||
name.name_type.0.hash(state);
|
||||
for name_string in &name.name_string.0.0 {
|
||||
name_string.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
hash_principal_name(&self.cname, state);
|
||||
hash_principal_name(&self.sname, state);
|
||||
self.ctime.hash(state);
|
||||
self.microseconds.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type AuthenticatorsCache = HashSet<AuthenticatorCacheRecord>;
|
||||
@@ -0,0 +1,57 @@
|
||||
use picky_krb::constants::key_usages::{AP_REQ_AUTHENTICATOR, TICKET_REP};
|
||||
use picky_krb::constants::types::{NT_ENTERPRISE, NT_PRINCIPAL};
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
use picky_krb::data_types::{Authenticator, EncTicketPart, KerberosStringAsn1, PrincipalName};
|
||||
use picky_krb::messages::ApReq;
|
||||
|
||||
use crate::{Error, ErrorKind, Result, Secret, Username};
|
||||
|
||||
/// Decrypts the [ApReq] ticket and returns decoded encrypted part of the ticket.
|
||||
pub(super) fn decrypt_ap_req_ticket(key: &Secret<Vec<u8>>, ap_req: &ApReq) -> Result<EncTicketPart> {
|
||||
let ticket_enc_part = &ap_req.0.ticket.0.0.enc_part.0;
|
||||
let cipher = CipherSuite::try_from(ticket_enc_part.etype.0.0.as_slice())?.cipher();
|
||||
|
||||
let encoded_enc_part = cipher.decrypt(key.as_ref(), TICKET_REP, &ticket_enc_part.cipher.0.0)?;
|
||||
|
||||
Ok(picky_asn1_der::from_bytes(&encoded_enc_part)?)
|
||||
}
|
||||
|
||||
/// Decrypts [ApReq] Authenticator and returns decoded authenticator.
|
||||
pub(super) fn decrypt_ap_req_authenticator(session_key: &Secret<Vec<u8>>, ap_req: &ApReq) -> Result<Authenticator> {
|
||||
let encrypted_authenticator = &ap_req.0.authenticator.0;
|
||||
let cipher = CipherSuite::try_from(encrypted_authenticator.etype.0.0.as_slice())?.cipher();
|
||||
|
||||
let encoded_authenticator = cipher.decrypt(
|
||||
session_key.as_ref(),
|
||||
AP_REQ_AUTHENTICATOR,
|
||||
&encrypted_authenticator.cipher.0.0,
|
||||
)?;
|
||||
|
||||
Ok(picky_asn1_der::from_bytes(&encoded_authenticator)?)
|
||||
}
|
||||
|
||||
/// Constructs [Username] from the client's [PrincipalName] and realm.
|
||||
pub(super) fn client_upn(cname: &PrincipalName, crealm: &KerberosStringAsn1) -> Result<Username> {
|
||||
let username = cname
|
||||
.name_string
|
||||
.0
|
||||
.0
|
||||
.first()
|
||||
.map(|name| name.to_string())
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing cname value in token"))?;
|
||||
|
||||
let name_type = &cname.name_type.0.0;
|
||||
if name_type == &[NT_PRINCIPAL] {
|
||||
Ok(Username::new_upn(
|
||||
&username,
|
||||
&crealm.0.to_string().to_ascii_lowercase(),
|
||||
)?)
|
||||
} else if name_type == &[NT_ENTERPRISE] {
|
||||
Ok(Username::parse(&username)?)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!("unsupported principal name type: {name_type:?}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use picky_asn1::wrapper::{
|
||||
ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, IntegerAsn1, OctetStringAsn1,
|
||||
Optional,
|
||||
};
|
||||
use picky_krb::constants::key_usages::AP_REP_ENC;
|
||||
use picky_krb::constants::types::{AP_REP_MSG_TYPE, TGT_REP_MSG_TYPE};
|
||||
use picky_krb::data_types::{
|
||||
EncApRepPart, EncApRepPartInner, EncryptedData, EncryptionKey, KerberosTime, Microseconds, Ticket,
|
||||
};
|
||||
use picky_krb::messages::{ApRep, ApRepInner, TgtRep};
|
||||
|
||||
use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EncryptionParams};
|
||||
use crate::{KERBEROS_VERSION, Result, Secret};
|
||||
|
||||
pub(super) fn generate_ap_rep(
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
ctime: KerberosTime,
|
||||
cusec: Microseconds,
|
||||
seq_number: Vec<u8>,
|
||||
enc_params: &EncryptionParams,
|
||||
) -> Result<ApRep> {
|
||||
let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE);
|
||||
|
||||
let enc_part = EncApRepPart::from(EncApRepPartInner {
|
||||
ctime: ExplicitContextTag0::from(ctime),
|
||||
cusec: ExplicitContextTag1::from(cusec),
|
||||
subkey: Optional::from(enc_params.sub_session_key.as_ref().map(|sub_key| {
|
||||
ExplicitContextTag2::from(EncryptionKey {
|
||||
key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])),
|
||||
key_value: ExplicitContextTag1::from(OctetStringAsn1::from(sub_key.as_ref().to_vec())),
|
||||
})
|
||||
})),
|
||||
seq_number: Optional::from(Some(ExplicitContextTag3::from(IntegerAsn1::from(seq_number)))),
|
||||
});
|
||||
|
||||
let cipher = encryption_type.cipher();
|
||||
let enc_data = cipher.encrypt(session_key.as_ref(), AP_REP_ENC, &picky_asn1_der::to_vec(&enc_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(enc_data)),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn generate_tgt_rep(ticket: Ticket) -> TgtRep {
|
||||
TgtRep {
|
||||
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
|
||||
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REP_MSG_TYPE])),
|
||||
ticket: ExplicitContextTag2::from(ticket),
|
||||
}
|
||||
}
|
||||
+547
@@ -0,0 +1,547 @@
|
||||
pub(crate) mod as_exchange;
|
||||
mod cache;
|
||||
mod extractors;
|
||||
mod generators;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use cache::AuthenticatorCacheRecord;
|
||||
use picky::oids;
|
||||
use picky_asn1::restricted_string::IA5String;
|
||||
use picky_asn1::wrapper::{Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, IntegerAsn1};
|
||||
use picky_krb::constants::gss_api::{AP_REP_TOKEN_ID, AP_REQ_TOKEN_ID, TGT_REP_TOKEN_ID, TGT_REQ_TOKEN_ID};
|
||||
use picky_krb::constants::types::NT_SRV_INST;
|
||||
use picky_krb::data_types::{AuthenticatorInner, KerberosStringAsn1, PrincipalName};
|
||||
use picky_krb::gss_api::MechTypeList;
|
||||
use picky_krb::messages::{ApRep, ApReq, TgtReq};
|
||||
use rand::rngs::{StdRng, SysRng};
|
||||
use rand_core::{Rng as _, SeedableRng as _};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use self::cache::AuthenticatorsCache;
|
||||
use self::extractors::{decrypt_ap_req_authenticator, decrypt_ap_req_ticket};
|
||||
use self::generators::generate_ap_rep;
|
||||
use crate::builders::FilledAcceptSecurityContext;
|
||||
use crate::generator::YieldPointLocal;
|
||||
use crate::kerberos::DEFAULT_ENCRYPTION_TYPE;
|
||||
use crate::kerberos::client::extractors::extract_seq_number_from_ap_rep;
|
||||
use crate::kerberos::flags::ApOptions;
|
||||
use crate::kerberos::messages::{decode_krb_message, generate_krb_message};
|
||||
use crate::kerberos::server::as_exchange::request_tgt;
|
||||
use crate::kerberos::server::extractors::client_upn;
|
||||
use crate::kerberos::server::generators::generate_tgt_rep;
|
||||
use crate::{
|
||||
AcceptSecurityContextResult, BufferType, CredentialsBuffers, Error, ErrorKind, Kerberos, KerberosState, Result,
|
||||
Secret, SecurityBuffer, SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, Username,
|
||||
};
|
||||
|
||||
/// Additional properties that are needed only for server-side Kerberos.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerProperties {
|
||||
/// Supported mech types sent by the client in the first incoming message.
|
||||
/// We user them for checksum calculation during MIC token generation.
|
||||
pub mech_types: MechTypeList,
|
||||
/// Maximum allowed time difference between client and server clocks.
|
||||
/// It is recommended to set this value not greater then a few minutes.
|
||||
pub max_time_skew: Duration,
|
||||
/// Key that is used for TGS tickets decryption.
|
||||
/// It should be provided by the user during regular Kerberos auth. Or
|
||||
/// it will be established during AS exchange in the case of Kerberos U2U auth.
|
||||
pub ticket_decryption_key: Option<Secret<Vec<u8>>>,
|
||||
/// Name of the Kerberos service.
|
||||
pub service_name: PrincipalName,
|
||||
/// Additional service principals this acceptor will honor, each paired
|
||||
/// with its own ticket-decryption key. A single keytab routinely holds
|
||||
/// keys for several host SPNs (e.g. `kafka/localhost` and
|
||||
/// `kafka/host.docker.internal`); the acceptor must validate an incoming
|
||||
/// AP-REQ against whichever SPN the client's ticket actually names — not
|
||||
/// just one pinned name — matching the behavior of MIT/Heimdal GSSAPI
|
||||
/// acceptors that key off the whole keytab.
|
||||
pub additional_service_keys: Vec<(PrincipalName, Secret<Vec<u8>>)>,
|
||||
/// User credentials on whose behalf the TGT ticket will be requested.
|
||||
pub user: Option<CredentialsBuffers>,
|
||||
/// Username of the authenticated client.
|
||||
///
|
||||
/// This field should be set by the Kerberos implementation after successful log on.
|
||||
pub client: Option<Username>,
|
||||
/// Authenticators cache.
|
||||
///
|
||||
/// [Receipt of KRB_AP_REQ Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3):
|
||||
///
|
||||
/// > The server MUST utilize a replay cache to remember any authenticator presented within the allowable clock skew.
|
||||
/// > The replay cache will store at least the server name, along with the client name, time,
|
||||
/// > and microsecond fields from the recently-seen authenticators, and if a matching tuple is found,
|
||||
/// > the error is returned.
|
||||
pub authenticators_cache: AuthenticatorsCache,
|
||||
}
|
||||
|
||||
impl ServerProperties {
|
||||
/// Creates a new instance of [ServerProperties].
|
||||
pub fn new(
|
||||
sname: &[&str],
|
||||
user: Option<CredentialsBuffers>,
|
||||
max_time_skew: Duration,
|
||||
ticket_decryption_key: Option<Secret<Vec<u8>>>,
|
||||
) -> Result<Self> {
|
||||
let service_names = sname
|
||||
.iter()
|
||||
.map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string((*sname).to_owned())?)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(Self {
|
||||
mech_types: MechTypeList::from(Vec::new()),
|
||||
max_time_skew,
|
||||
ticket_decryption_key,
|
||||
service_name: PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(service_names)),
|
||||
},
|
||||
additional_service_keys: Vec::new(),
|
||||
user,
|
||||
client: None,
|
||||
authenticators_cache: AuthenticatorsCache::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Register an additional service principal (by name components, without
|
||||
/// realm) and its ticket-decryption key. The acceptor will accept an
|
||||
/// AP-REQ whose ticket names this principal in addition to the primary
|
||||
/// [`service_name`](Self::service_name).
|
||||
pub fn add_service_key(&mut self, sname: &[&str], key: Secret<Vec<u8>>) -> Result<()> {
|
||||
let service_names = sname
|
||||
.iter()
|
||||
.map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string((*sname).to_owned())?)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let principal = PrincipalName {
|
||||
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
|
||||
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(service_names)),
|
||||
};
|
||||
self.additional_service_keys.push((principal, key));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve the ticket-decryption key for an incoming AP-REQ whose ticket
|
||||
/// names `ticket_sname`. Matches the requested service principal against
|
||||
/// the primary [`service_name`](Self::service_name) and any
|
||||
/// [`additional_service_keys`](Self::additional_service_keys), comparing
|
||||
/// name components only (ignoring the PrincipalName `name-type`, per
|
||||
/// RFC 4120 §6.2). Returns the matching key, or `None` if no configured
|
||||
/// service principal matches.
|
||||
pub fn ticket_decryption_key_for(&self, ticket_sname: &PrincipalName) -> Option<&Secret<Vec<u8>>> {
|
||||
if self.service_name.name_string.0 == ticket_sname.name_string.0
|
||||
&& let Some(key) = self.ticket_decryption_key.as_ref()
|
||||
{
|
||||
return Some(key);
|
||||
}
|
||||
self.additional_service_keys
|
||||
.iter()
|
||||
.find(|(spn, _)| spn.name_string.0 == ticket_sname.name_string.0)
|
||||
.map(|(_, key)| key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs one authentication step.
|
||||
///
|
||||
/// The user should call this function until it returns `SecurityStatus::Ok`.
|
||||
pub async fn accept_security_context(
|
||||
server: &mut Kerberos,
|
||||
yield_point: &mut YieldPointLocal,
|
||||
builder: FilledAcceptSecurityContext<'_, <Kerberos as SspiImpl>::CredentialsHandle>,
|
||||
) -> Result<AcceptSecurityContextResult> {
|
||||
let input = builder
|
||||
.input
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?;
|
||||
let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?;
|
||||
|
||||
if server.state == KerberosState::TgtExchange {
|
||||
if let Ok(tgt_req) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
|
||||
picky_asn1_der::from_bytes::<TgtReq>(&input_token.buffer).map_err(Error::from)
|
||||
} else {
|
||||
decode_krb_message::<TgtReq>(&input_token.buffer, TGT_REQ_TOKEN_ID)
|
||||
} {
|
||||
// The first token is TGT_REQ. It means that the client wants to perform Kerberos U2U.
|
||||
|
||||
if !builder
|
||||
.context_requirements
|
||||
.contains(ServerRequestFlags::USE_SESSION_KEY)
|
||||
{
|
||||
warn!(
|
||||
"KRB5 U2U has been negotiated (requested by the client) but the USE_SESSION_KEY flag is not set."
|
||||
);
|
||||
}
|
||||
|
||||
server.krb5_user_to_user = true;
|
||||
|
||||
let credentials = server
|
||||
.server
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "Kerberos server configuration not present"))?
|
||||
.user
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(ErrorKind::IncompleteCredentials, "KRB5 U2U has been negotiated (requested by the client) but the user credentials are not preset in Kerberos server configuration"))?
|
||||
.clone();
|
||||
|
||||
let tgt_rep = generate_tgt_rep(request_tgt(server, &credentials, &tgt_req, yield_point).await?);
|
||||
|
||||
let mech_id = if server.krb5_user_to_user {
|
||||
oids::krb5_user_to_user()
|
||||
} else {
|
||||
oids::krb5()
|
||||
};
|
||||
|
||||
let encoded_tgt_rep = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
|
||||
picky_asn1_der::to_vec(&tgt_rep)?
|
||||
} else {
|
||||
generate_krb_message(mech_id, TGT_REP_TOKEN_ID, tgt_rep)?
|
||||
};
|
||||
|
||||
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
|
||||
output_token.buffer = encoded_tgt_rep;
|
||||
|
||||
server.state = KerberosState::Preauthentication;
|
||||
|
||||
return Ok(AcceptSecurityContextResult {
|
||||
status: SecurityStatus::ContinueNeeded,
|
||||
flags: ServerResponseFlags::empty(),
|
||||
expiry: None,
|
||||
});
|
||||
} else if let Ok(_ap_req) = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
|
||||
picky_asn1_der::from_bytes::<ApReq>(&input_token.buffer).map_err(Error::from)
|
||||
} else {
|
||||
decode_krb_message::<ApReq>(&input_token.buffer, AP_REQ_TOKEN_ID)
|
||||
} {
|
||||
// The client may send ApReq instead of TgtReq in the first message.
|
||||
// It means that the client wants to perform regular Kerberos without U2U.
|
||||
// In that case, we just move Kerberos state to the next one and process further.
|
||||
server.state = KerberosState::Preauthentication;
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"invalid Kerberos token: expected TgtReq or ApReq",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let status =
|
||||
match server.state {
|
||||
KerberosState::Preauthentication => {
|
||||
let ap_req = if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
|
||||
picky_asn1_der::from_bytes::<ApReq>(&input_token.buffer)?
|
||||
} else {
|
||||
decode_krb_message::<ApReq>(&input_token.buffer, AP_REQ_TOKEN_ID)?
|
||||
};
|
||||
|
||||
let server_data = server.server.as_ref().ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidHandle,
|
||||
"Kerberos server properties are not initialized",
|
||||
)
|
||||
})?;
|
||||
|
||||
let ticket_service_name = &ap_req.0.ticket.0.0.sname.0;
|
||||
// Match the service principal by its name components only, ignoring
|
||||
// the PrincipalName `name-type`. RFC 4120 §6.2 treats the name-type
|
||||
// as a hint about how to interpret the components, not as part of
|
||||
// the principal's identity, and acceptors (MIT, Heimdal) key off
|
||||
// the components. Stock Kafka GSSAPI clients build the server
|
||||
// principal as NT-UNKNOWN/NT-PRINCIPAL while a `kafka/host` keytab
|
||||
// entry is NT-SRV-INST, so a name-type-sensitive comparison would
|
||||
// spuriously reject otherwise-valid tickets.
|
||||
//
|
||||
// A keytab may carry keys for several host SPNs; accept whichever
|
||||
// one the ticket actually names and decrypt with its key.
|
||||
let ticket_decryption_key = server_data.ticket_decryption_key_for(ticket_service_name).ok_or_else(
|
||||
|| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!(
|
||||
"invalid ticket service name ({:?}): no matching service key configured (primary {:?})",
|
||||
ticket_service_name, server_data.service_name
|
||||
),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
let ticket_enc_part = decrypt_ap_req_ticket(ticket_decryption_key, &ap_req)?;
|
||||
let session_key = Secret::new(ticket_enc_part.0.key.0.key_value.0.0.clone());
|
||||
|
||||
let AuthenticatorInner {
|
||||
authenticator_vno: _,
|
||||
crealm,
|
||||
cname,
|
||||
cksum: _,
|
||||
cusec,
|
||||
ctime,
|
||||
subkey,
|
||||
seq_number,
|
||||
authorization_data: _,
|
||||
} = decrypt_ap_req_authenticator(&session_key, &ap_req)?.0;
|
||||
|
||||
// RFC 4121 §4.2: if the initiator places a subkey in its
|
||||
// Authenticator and the acceptor returns no subkey of its own
|
||||
// (i.e. no mutual-auth AP-REP), that initiator subkey — not the
|
||||
// ticket session key — becomes the key for per-message (wrap/MIC)
|
||||
// tokens. Stock Java/Kafka GSSAPI clients always send an
|
||||
// Authenticator subkey, so dropping it makes the security-layer
|
||||
// wrap fail the client's checksum. Capture it here; the non-mutual
|
||||
// branch below installs it as the sub-session key.
|
||||
let authenticator_subkey: Option<Secret<Vec<u8>>> =
|
||||
subkey.0.as_ref().map(|key| Secret::new(key.0.key_value.0.0.clone()));
|
||||
|
||||
// The initiator's Authenticator sequence number. Without a mutual-
|
||||
// auth AP-REP to carry an acceptor sequence number, GSS initiators
|
||||
// (e.g. Java) initialise the *expected* acceptor sequence to their
|
||||
// own initiator value, so the acceptor's per-message tokens must be
|
||||
// numbered from it or the initiator rejects them as "gap" tokens.
|
||||
let authenticator_seq_number: Option<u32> = seq_number.0.as_ref().map(|seq| {
|
||||
let seq_number_bytes = &seq.0.0;
|
||||
// IntegerAsn1 is big-endian and may carry a leading 0x00 sign
|
||||
// byte; take the low 4 octets.
|
||||
let mut buf = [0u8; 4];
|
||||
let start = seq_number_bytes.len().saturating_sub(4);
|
||||
let slice = &seq_number_bytes[start..];
|
||||
buf[4 - slice.len()..].copy_from_slice(slice);
|
||||
u32::from_be_bytes(buf)
|
||||
});
|
||||
|
||||
// [3.2.3. Receipt of KRB_AP_REQ Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3)
|
||||
// The name and realm of the client from the ticket are compared against the same fields in the authenticator.
|
||||
if ticket_enc_part.0.crealm.0 != crealm.0 || ticket_enc_part.0.cname != cname.0 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"the name and realm of the client in ticket and authenticator do not match",
|
||||
));
|
||||
}
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let client_time = OffsetDateTime::try_from(ctime.0.0.clone())
|
||||
.map_err(|err| Error::new(ErrorKind::InvalidToken, format!("client time is not valid: {err:?}")))?;
|
||||
let max_time_skew = server_data.max_time_skew;
|
||||
|
||||
if (now - client_time).abs() > max_time_skew {
|
||||
return Err(Error::new(
|
||||
ErrorKind::TimeSkew,
|
||||
"invalid authenticator ctime: time skew is too big",
|
||||
));
|
||||
}
|
||||
|
||||
let ticket_start_time = ticket_enc_part
|
||||
.0
|
||||
.starttime
|
||||
.0
|
||||
.map(|start_time| start_time.0)
|
||||
// [5.3. Tickets](https://www.rfc-editor.org/rfc/rfc4120#section-5.3)
|
||||
// If the starttime field is absent from the ticket, then the authtime field SHOULD be used in its place to determine
|
||||
// the life of the ticket.
|
||||
.unwrap_or_else(|| ticket_enc_part.0.auth_time.0)
|
||||
.0;
|
||||
let ticket_start_time = OffsetDateTime::try_from(ticket_start_time).map_err(|err| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!("ticket end time is not valid: {err:?}"),
|
||||
)
|
||||
})?;
|
||||
if ticket_start_time > now + max_time_skew {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"ticket not yet valid: ticket start time is greater than current time + max time skew",
|
||||
));
|
||||
}
|
||||
|
||||
let ticket_end_time = OffsetDateTime::try_from(ticket_enc_part.0.endtime.0.0).map_err(|err| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!("ticket end time is not valid: {err:?}"),
|
||||
)
|
||||
})?;
|
||||
if now > ticket_end_time + max_time_skew {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"ticket is expired: current time is greater than ticket end time + max time skew",
|
||||
));
|
||||
}
|
||||
|
||||
let server_data = server.server.as_mut().ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidHandle,
|
||||
"Kerberos server properties are not initialized",
|
||||
)
|
||||
})?;
|
||||
|
||||
let cache_record = AuthenticatorCacheRecord {
|
||||
cname: cname.0.clone(),
|
||||
sname: ticket_service_name.clone(),
|
||||
ctime: ctime.0.clone(),
|
||||
microseconds: cusec.0.clone(),
|
||||
};
|
||||
if !server_data.authenticators_cache.contains(&cache_record) {
|
||||
server_data.authenticators_cache.insert(cache_record);
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"ApReq Authenticator replay detected",
|
||||
));
|
||||
}
|
||||
|
||||
debug!("ApReq Ticket and Authenticator are valid!");
|
||||
|
||||
server_data.client = Some(client_upn(&cname.0, &crealm.0)?);
|
||||
|
||||
let ap_options_bytes = ap_req.0.ap_options.0.0.as_bytes();
|
||||
// [5.5.1. KRB_AP_REQ Definition](https://www.rfc-editor.org/rfc/rfc4120#section-5.5.1)
|
||||
// The `ap-options` field has 32 bits or 4 bytes long. But it is encoded as BitStringAsn1, so the first byte
|
||||
// indicates the number of bits used. Thus, the overall number of expected bytes is 1 + 4 = 5.
|
||||
if ap_options_bytes.len() != 1 + 4 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!(
|
||||
"invalid ApReq ap-options: invalid data length: expected 5 bytes but got {}",
|
||||
ap_options_bytes.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
let ap_options = u32::from_be_bytes(ap_options_bytes[1..].try_into().map_err(|err| {
|
||||
Error::new(ErrorKind::InvalidToken, format!("invalid ApReq ap-options: {err:?}"))
|
||||
})?);
|
||||
let ap_options = ApOptions::from_bits(ap_options)
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "invalid ApReq ap-options"))?;
|
||||
|
||||
// [3.2.4. Generation of a KRB_AP_REP Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3)
|
||||
// ...the server need not explicitly reply to the KRB_AP_REQ. However, if mutual authentication is being performed,
|
||||
// the KRB_AP_REQ message will have MUTUAL-REQUIRED set in its ap-options field, and a KRB_AP_REP message
|
||||
// is required in response.
|
||||
let status = if ap_options.contains(ApOptions::MUTUAL_REQUIRED) {
|
||||
let key_size = server
|
||||
.encryption_params
|
||||
.encryption_type
|
||||
.as_ref()
|
||||
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
|
||||
.cipher()
|
||||
.key_size();
|
||||
let mut sub_session_key = vec![0; key_size];
|
||||
|
||||
let mut rand = StdRng::try_from_rng(&mut SysRng)?;
|
||||
rand.fill_bytes(&mut sub_session_key);
|
||||
server.encryption_params.sub_session_key = Some(sub_session_key.into());
|
||||
|
||||
// [3.2.4. Generation of a KRB_AP_REP Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3)
|
||||
// A subkey MAY be included if the server desires to negotiate a different subkey.
|
||||
// The KRB_AP_REP message is encrypted in the session key extracted from the ticket.
|
||||
let ap_rep = generate_ap_rep(
|
||||
&session_key,
|
||||
ctime.0,
|
||||
cusec.0,
|
||||
(server.seq_number + 1).to_be_bytes().to_vec(),
|
||||
&server.encryption_params,
|
||||
)?;
|
||||
|
||||
let mech_id = if server.krb5_user_to_user {
|
||||
oids::krb5_user_to_user()
|
||||
} else {
|
||||
oids::krb5()
|
||||
};
|
||||
|
||||
let (status, encoded_ap_rep) =
|
||||
if builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
|
||||
let encoded_ap_rep = picky_asn1_der::to_vec(&ap_rep)?;
|
||||
server.state = KerberosState::ApExchange;
|
||||
|
||||
(SecurityStatus::ContinueNeeded, encoded_ap_rep)
|
||||
} else {
|
||||
let encoded_ap_rep = generate_krb_message(mech_id, AP_REP_TOKEN_ID, ap_rep)?;
|
||||
server.state = KerberosState::Final;
|
||||
|
||||
(SecurityStatus::Ok, encoded_ap_rep)
|
||||
};
|
||||
|
||||
let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?;
|
||||
output_token.buffer = encoded_ap_rep;
|
||||
|
||||
status
|
||||
} else {
|
||||
// No mutual auth requested: the server emits no AP-REP, but the
|
||||
// GSS context is nonetheless established from a valid AP-REQ
|
||||
// (RFC 4120 §3.2.4 — MUTUAL-REQUIRED only governs whether an
|
||||
// AP-REP is returned, not whether the context is usable). Move
|
||||
// to Final so subsequent wrap/unwrap (e.g. the SASL GSSAPI
|
||||
// security-layer negotiation) succeed instead of erroring with
|
||||
// "context is not established".
|
||||
//
|
||||
// Since we return no acceptor subkey, per-message tokens use the
|
||||
// initiator's Authenticator subkey if it provided one (RFC 4121
|
||||
// §4.2), falling back to the ticket session key otherwise.
|
||||
if let Some(subkey) = authenticator_subkey {
|
||||
server.encryption_params.sub_session_key = Some(subkey);
|
||||
}
|
||||
|
||||
// Number our outgoing per-message tokens from the initiator's
|
||||
// Authenticator sequence number. With no AP-REP to carry an
|
||||
// acceptor sequence, GSS initiators set their *expected*
|
||||
// acceptor sequence equal to their own initiator value, so the
|
||||
// first acceptor wrap token must use that number or the
|
||||
// initiator rejects it as a "gap" token. `next_seq_number()`
|
||||
// pre-increments, so seed one below.
|
||||
if let Some(seq) = authenticator_seq_number {
|
||||
server.seq_number = seq.wrapping_sub(1);
|
||||
}
|
||||
server.state = KerberosState::Final;
|
||||
|
||||
SecurityStatus::Ok
|
||||
};
|
||||
|
||||
server.encryption_params.session_key = Some(session_key);
|
||||
|
||||
status
|
||||
}
|
||||
KerberosState::ApExchange => {
|
||||
if !builder.context_requirements.contains(ServerRequestFlags::USE_DCE_STYLE) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::OutOfSequence,
|
||||
"USE_DCE_STYLE flag must be set in context requirements",
|
||||
));
|
||||
}
|
||||
|
||||
let ap_rep = picky_asn1_der::from_bytes::<ApRep>(&input_token.buffer)?;
|
||||
|
||||
let session_key = server
|
||||
.encryption_params
|
||||
.session_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?;
|
||||
let seq_number = extract_seq_number_from_ap_rep(&ap_rep, session_key, &server.encryption_params)?;
|
||||
let seq_number = u32::from_be_bytes(seq_number.try_into().map_err(|err| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!("invalid ApRep sequence number: {:?}", err),
|
||||
)
|
||||
})?);
|
||||
|
||||
let expected_seq_number = server.seq_number + 1;
|
||||
if seq_number != expected_seq_number {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
format!(
|
||||
"invalid client ApRep sequence number: expected {expected_seq_number} but got {seq_number}",
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
server.state = KerberosState::Final;
|
||||
|
||||
SecurityStatus::Ok
|
||||
}
|
||||
KerberosState::Final | KerberosState::TgtExchange => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::OutOfSequence,
|
||||
format!("got wrong Kerberos state: {:?}", server.state),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(AcceptSecurityContextResult {
|
||||
status,
|
||||
flags: ServerResponseFlags::empty(),
|
||||
expiry: None,
|
||||
})
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
use picky_krb::constants::key_usages::{ACCEPTOR_SEAL, INITIATOR_SEAL};
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
|
||||
use crate::kerberos::{EncryptionParams, KerberosConfig, KerberosState, test_data};
|
||||
use crate::{EncryptionFlags, Kerberos, SecurityBufferFlags, SecurityBufferRef, Sspi};
|
||||
|
||||
#[test]
|
||||
fn stream_buffer_decryption() {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi
|
||||
|
||||
let mut kerberos_server = test_data::fake_server();
|
||||
let mut kerberos_client = test_data::fake_client();
|
||||
|
||||
let plain_message = b"some plain message";
|
||||
|
||||
let mut token = [0; 1024];
|
||||
let mut data = plain_message.to_vec();
|
||||
let mut message = [
|
||||
SecurityBufferRef::token_buf(token.as_mut_slice()),
|
||||
SecurityBufferRef::data_buf(data.as_mut_slice()),
|
||||
];
|
||||
|
||||
kerberos_server
|
||||
.encrypt_message(EncryptionFlags::empty(), &mut message)
|
||||
.unwrap();
|
||||
|
||||
let mut buffer = message[0].data().to_vec();
|
||||
buffer.extend_from_slice(message[1].data());
|
||||
|
||||
let mut message = [
|
||||
SecurityBufferRef::stream_buf(&mut buffer),
|
||||
SecurityBufferRef::data_buf(&mut []),
|
||||
];
|
||||
|
||||
kerberos_client.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(message[1].data(), plain_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secbuffer_readonly_with_checksum() {
|
||||
// All values in this test (session keys, sequence number, encrypted and decrypted data) were extracted
|
||||
// from the original Windows Kerberos implementation calls.
|
||||
// We keep this test to guarantee full compatibility with the original Kerberos.
|
||||
|
||||
let session_key = [
|
||||
114, 67, 55, 26, 76, 210, 61, 0, 164, 44, 11, 133, 108, 220, 234, 145, 61, 144, 123, 45, 54, 175, 164, 168, 99,
|
||||
18, 99, 240, 242, 157, 95, 134,
|
||||
];
|
||||
let sub_session_key = [
|
||||
91, 11, 188, 227, 10, 91, 180, 246, 64, 129, 251, 200, 118, 82, 109, 65, 241, 177, 109, 32, 124, 39, 127, 171,
|
||||
222, 132, 199, 199, 126, 110, 3, 166,
|
||||
];
|
||||
|
||||
let mut kerberos_server = Kerberos {
|
||||
state: KerberosState::Final,
|
||||
config: KerberosConfig {
|
||||
kdc_url: None,
|
||||
client_computer_name: "hostname".into(),
|
||||
},
|
||||
auth_identity: None,
|
||||
encryption_params: EncryptionParams {
|
||||
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
|
||||
session_key: Some(session_key.to_vec().into()),
|
||||
sub_session_key: Some(sub_session_key.to_vec().into()),
|
||||
sspi_encrypt_key_usage: ACCEPTOR_SEAL,
|
||||
sspi_decrypt_key_usage: INITIATOR_SEAL,
|
||||
ec: 16,
|
||||
},
|
||||
seq_number: 681238048,
|
||||
realm: None,
|
||||
kdc_url: None,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: Some(Box::new(test_data::fake_server_properties())),
|
||||
};
|
||||
|
||||
// RPC header
|
||||
let header = [
|
||||
5, 0, 0, 3, 16, 0, 0, 0, 60, 1, 76, 0, 1, 0, 0, 0, 208, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// RPC security trailer header
|
||||
let trailer = [16, 6, 8, 0, 0, 0, 0, 0];
|
||||
// Encrypted data in RPC Request
|
||||
let enc_data = [
|
||||
41, 85, 192, 239, 104, 188, 180, 100, 229, 73, 83, 199, 77, 83, 79, 17, 163, 206, 241, 29, 90, 28, 89, 203, 83,
|
||||
176, 160, 252, 197, 221, 76, 113, 185, 141, 16, 200, 149, 55, 32, 96, 29, 49, 57, 124, 181, 147, 110, 198, 125,
|
||||
116, 150, 47, 35, 224, 117, 25, 10, 229, 201, 222, 153, 101, 131, 93, 204, 32, 9, 145, 186, 45, 224, 160, 131,
|
||||
23, 236, 111, 88, 48, 54, 4, 118, 114, 129, 119, 130, 164, 178, 4, 110, 74, 37, 1, 215, 177, 16, 204, 238, 83,
|
||||
255, 40, 240, 32, 209, 213, 90, 19, 126, 58, 34, 33, 72, 15, 206, 96, 67, 15, 169, 248, 176, 9, 173, 196, 159,
|
||||
239, 250, 120, 206, 52, 53, 229, 230, 66, 64, 109, 100, 21, 77, 193, 3, 40, 183, 209, 177, 152, 165, 171, 108,
|
||||
151, 112, 134, 53, 165, 128, 145, 147, 167, 5, 72, 35, 101, 42, 183, 67, 101, 48, 255, 84, 208, 112, 199, 154,
|
||||
62, 185, 87, 204, 228, 45, 30, 184, 47, 129, 145, 245, 168, 118, 174, 48, 98, 174, 167, 208, 0, 113, 246, 219,
|
||||
29, 192, 171, 97, 117, 115, 120, 115, 45, 44, 113, 62, 39,
|
||||
];
|
||||
// Unencrypted data in RPC Request
|
||||
let plaintext = [
|
||||
108, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 128, 84, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 20, 0,
|
||||
0, 0, 2, 0, 64, 0, 2, 0, 0, 0, 0, 0, 36, 0, 3, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 223, 243, 137, 88,
|
||||
86, 131, 83, 53, 105, 218, 109, 33, 80, 4, 0, 0, 0, 0, 20, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 138, 227, 19, 113, 2, 244, 54, 113, 2, 64, 40, 0,
|
||||
96, 89, 120, 185, 79, 82, 223, 17, 139, 109, 131, 220, 222, 215, 32, 133, 1, 0, 0, 0, 51, 5, 113, 113, 186,
|
||||
190, 55, 73, 131, 25, 181, 219, 239, 156, 204, 54, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// RPC Request security trailer data. Basically, it's a GSS API Wrap token
|
||||
let security_trailer_data = [
|
||||
5, 4, 6, 255, 0, 16, 0, 28, 0, 0, 0, 0, 40, 154, 222, 33, 170, 177, 218, 93, 176, 5, 210, 44, 38, 242, 179,
|
||||
168, 249, 202, 242, 199, 63, 162, 33, 40, 106, 186, 187, 28, 11, 229, 207, 219, 66, 86, 243, 16, 158, 100, 133,
|
||||
159, 87, 153, 196, 14, 251, 169, 164, 12, 18, 85, 182, 56, 72, 30, 137, 238, 50, 122, 73, 95, 109, 194, 60,
|
||||
120,
|
||||
];
|
||||
|
||||
let mut header_data = header.to_vec();
|
||||
let mut encrypted_data = enc_data.to_vec();
|
||||
let mut trailer_data = trailer.to_vec();
|
||||
let mut token_data = security_trailer_data.to_vec();
|
||||
let mut message = vec![
|
||||
SecurityBufferRef::data_buf(&mut header_data).with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::data_buf(&mut encrypted_data),
|
||||
SecurityBufferRef::data_buf(&mut trailer_data)
|
||||
.with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::token_buf(&mut token_data),
|
||||
];
|
||||
|
||||
kerberos_server.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(header[..], message[0].data()[..]);
|
||||
assert_eq!(plaintext[..], message[1].data()[..]);
|
||||
assert_eq!(trailer[..], message[2].data()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_request_encryption() {
|
||||
let mut kerberos_server = test_data::fake_server();
|
||||
let mut kerberos_client = test_data::fake_client();
|
||||
|
||||
// RPC header
|
||||
let header = [
|
||||
5, 0, 0, 3, 16, 0, 0, 0, 60, 1, 76, 0, 1, 0, 0, 0, 208, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// Unencrypted data in RPC Request
|
||||
let plaintext = [
|
||||
108, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 128, 84, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 20, 0,
|
||||
0, 0, 2, 0, 64, 0, 2, 0, 0, 0, 0, 0, 36, 0, 3, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 223, 243, 137, 88,
|
||||
86, 131, 83, 53, 105, 218, 109, 33, 80, 4, 0, 0, 0, 0, 20, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 138, 227, 19, 113, 2, 244, 54, 113, 2, 64, 40, 0,
|
||||
96, 89, 120, 185, 79, 82, 223, 17, 139, 109, 131, 220, 222, 215, 32, 133, 1, 0, 0, 0, 51, 5, 113, 113, 186,
|
||||
190, 55, 73, 131, 25, 181, 219, 239, 156, 204, 54, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// RPC security trailer header
|
||||
let trailer = [16, 6, 8, 0, 0, 0, 0, 0];
|
||||
|
||||
let mut header_data = header.to_vec();
|
||||
let mut data = plaintext.to_vec();
|
||||
let mut trailer_data = trailer.to_vec();
|
||||
let mut token_data = vec![0; 76];
|
||||
let mut message = vec![
|
||||
SecurityBufferRef::data_buf(&mut header_data).with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::data_buf(&mut data),
|
||||
SecurityBufferRef::data_buf(&mut trailer_data)
|
||||
.with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::token_buf(&mut token_data),
|
||||
];
|
||||
|
||||
kerberos_client
|
||||
.encrypt_message(EncryptionFlags::empty(), &mut message)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(header[..], message[0].data()[..]);
|
||||
assert_eq!(trailer[..], message[2].data()[..]);
|
||||
|
||||
kerberos_server.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(header[..], message[0].data()[..]);
|
||||
assert_eq!(message[1].data(), plaintext);
|
||||
assert_eq!(trailer[..], message[2].data()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrity_only_wrap_decryption() {
|
||||
// A standards-compliant SASL/GSSAPI peer (RFC 4752 / RFC 4121 §4.2.4) may
|
||||
// send an integrity-only (conf=false) Wrap token: the payload is carried in
|
||||
// the clear followed by the get_mic checksum, right-rotated by RRC. This
|
||||
// exercises the unsealed WRAP verification path (RRC undo, EC/RRC header
|
||||
// zeroing for the checksum input, and checksum validation) by feeding a
|
||||
// known-good token built the way a compliant sender would and asserting the
|
||||
// cleartext is recovered.
|
||||
use picky_krb::crypto::aes::{AesSize, checksum_sha_aes};
|
||||
use picky_krb::gss_api::WrapToken;
|
||||
|
||||
// Arbitrary AES-256 session key.
|
||||
let session_key = [
|
||||
0x9e, 0x8f, 0x7d, 0x6c, 0x5b, 0x4a, 0x39, 0x28, 0x17, 0x06, 0xf5, 0xe4, 0xd3, 0xc2, 0xb1, 0xa0, 0x10, 0x21,
|
||||
0x32, 0x43, 0x54, 0x65, 0x76, 0x87, 0x98, 0xa9, 0xba, 0xcb, 0xdc, 0xed, 0xfe, 0x0f,
|
||||
];
|
||||
|
||||
let plaintext: &[u8] = b"GSSAPI integrity-only payload";
|
||||
|
||||
// The acceptor sends the token; a client (initiator) decrypts it with the
|
||||
// ACCEPTOR_SEAL key usage. Wrap tokens — sealed or not — use the SEAL usage.
|
||||
let key_usage = ACCEPTOR_SEAL;
|
||||
let aes_size = AesSize::Aes256;
|
||||
|
||||
// The checksum is computed over `plaintext | header`, with the EC and RRC
|
||||
// fields of the 16-octet header zeroed.
|
||||
let seq_num = 42;
|
||||
let mut wrap_token = WrapToken::with_seq_number(seq_num);
|
||||
// SentByAcceptor (0x01) set, Sealed (0x02) clear -> integrity-only.
|
||||
wrap_token.flags = 0x01;
|
||||
wrap_token.ec = 0;
|
||||
wrap_token.rrc = 0;
|
||||
|
||||
let mut to_sign = plaintext.to_vec();
|
||||
to_sign.extend_from_slice(&wrap_token.header());
|
||||
let checksum = checksum_sha_aes(&session_key, key_usage, &to_sign, &aes_size).unwrap();
|
||||
|
||||
// For unsealed tokens, EC encodes the trailing checksum length (RFC 4121
|
||||
// §4.2.3). The sender right-rotates `plaintext | checksum` by RRC.
|
||||
let rrc = u16::try_from(checksum.len()).unwrap();
|
||||
let mut payload = plaintext.to_vec();
|
||||
payload.extend_from_slice(&checksum);
|
||||
payload.rotate_right(usize::from(rrc));
|
||||
|
||||
wrap_token.ec = rrc;
|
||||
wrap_token.rrc = rrc;
|
||||
wrap_token.checksum = payload;
|
||||
|
||||
let mut token_bytes = Vec::new();
|
||||
wrap_token.encode(&mut token_bytes).unwrap();
|
||||
|
||||
let mut kerberos_client = Kerberos {
|
||||
state: KerberosState::Final,
|
||||
config: KerberosConfig {
|
||||
kdc_url: None,
|
||||
client_computer_name: "hostname".into(),
|
||||
},
|
||||
auth_identity: None,
|
||||
encryption_params: EncryptionParams {
|
||||
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
|
||||
session_key: None,
|
||||
sub_session_key: Some(session_key.to_vec().into()),
|
||||
sspi_encrypt_key_usage: INITIATOR_SEAL,
|
||||
sspi_decrypt_key_usage: ACCEPTOR_SEAL,
|
||||
ec: 0,
|
||||
},
|
||||
seq_number: 0,
|
||||
realm: None,
|
||||
kdc_url: None,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: None,
|
||||
};
|
||||
|
||||
let mut buffer = token_bytes;
|
||||
let mut message = [
|
||||
SecurityBufferRef::stream_buf(&mut buffer),
|
||||
SecurityBufferRef::data_buf(&mut []),
|
||||
];
|
||||
|
||||
kerberos_client.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(message[1].data(), plaintext);
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
use std::io::Write;
|
||||
|
||||
use picky_krb::constants::key_usages::{ACCEPTOR_SIGN, INITIATOR_SIGN};
|
||||
use picky_krb::crypto::aes::{AesSize, checksum_sha_aes};
|
||||
use picky_krb::gss_api::MicToken;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::kerberos::encryption_params::EncryptionParams;
|
||||
use crate::{Error, ErrorKind, Result, Secret};
|
||||
|
||||
pub(super) fn serialize_message<T: ?Sized + Serialize>(v: &T) -> Result<Vec<u8>> {
|
||||
let mut data = Vec::new();
|
||||
// 4 bytes: length of the message
|
||||
data.write_all(&[0, 0, 0, 0])?;
|
||||
|
||||
picky_asn1_der::to_writer(v, &mut data)?;
|
||||
|
||||
let len = data.len() as u32 - 4;
|
||||
data[0..4].copy_from_slice(&len.to_be_bytes());
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub(super) fn validate_mic_token(
|
||||
is_client: bool,
|
||||
raw_token: &[u8],
|
||||
params: &EncryptionParams,
|
||||
mech_types: &[u8],
|
||||
) -> Result<()> {
|
||||
let key_usage = if is_client { ACCEPTOR_SIGN } else { INITIATOR_SIGN };
|
||||
|
||||
let token = MicToken::decode(raw_token)?;
|
||||
let token_flags = token.flags;
|
||||
|
||||
// [Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.2):
|
||||
//
|
||||
// The meanings of bits in this field (the least significant bit is bit
|
||||
// 0) are as follows:
|
||||
// Bit Name Description
|
||||
// --------------------------------------------------------------
|
||||
// 0 SentByAcceptor When set, this flag indicates the sender
|
||||
// is the context acceptor. When not set,
|
||||
// it indicates the sender is the context
|
||||
// initiator.
|
||||
if token_flags & 0b01 != u8::from(is_client) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"invalid MIC token SentByAcceptor flag",
|
||||
));
|
||||
}
|
||||
// 1 Sealed When set in Wrap tokens, this flag
|
||||
// indicates confidentiality is provided
|
||||
// for. It SHALL NOT be set in MIC tokens.
|
||||
if token_flags & 0b10 == 0b10 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"the Sealed flag has not to be set in the MIC token",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = [mech_types, token.header().as_slice()].concat();
|
||||
|
||||
// The sub-session key is always preferred over the session key.
|
||||
let key = if let Some(key) = params.sub_session_key.as_ref() {
|
||||
key
|
||||
} else if let Some(key) = params.session_key.as_ref() {
|
||||
key
|
||||
} else {
|
||||
return Err(Error::new(ErrorKind::DecryptFailure, "unable to obtain decryption key"));
|
||||
};
|
||||
|
||||
let checksum = checksum_sha_aes(
|
||||
key.as_ref(),
|
||||
key_usage,
|
||||
&payload,
|
||||
¶ms.aes_size().unwrap_or(AesSize::Aes256),
|
||||
)?;
|
||||
|
||||
if checksum != token.checksum {
|
||||
return Err(Error::new(ErrorKind::MessageAltered, "bad checksum of the mic token"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn generate_mic_token(
|
||||
is_client: bool,
|
||||
seq_number: u64,
|
||||
mut payload: Vec<u8>,
|
||||
session_key: &Secret<Vec<u8>>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let (mic_token, key_usage) = if is_client {
|
||||
(MicToken::with_initiator_flags(), INITIATOR_SIGN)
|
||||
} else {
|
||||
(MicToken::with_acceptor_flags(), ACCEPTOR_SIGN)
|
||||
};
|
||||
|
||||
let mut mic_token = mic_token.with_seq_number(seq_number);
|
||||
|
||||
payload.extend_from_slice(&mic_token.header());
|
||||
|
||||
mic_token.set_checksum(checksum_sha_aes(
|
||||
session_key.as_ref(),
|
||||
key_usage,
|
||||
&payload,
|
||||
&AesSize::Aes256,
|
||||
)?);
|
||||
|
||||
let mut mic_token_raw = Vec::new();
|
||||
mic_token.encode(&mut mic_token_raw)?;
|
||||
|
||||
Ok(mic_token_raw)
|
||||
}
|
||||
Reference in New Issue
Block a user