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:
Andy Dixon
2026-10-12 10:50:24 +01:00
parent 6a4d7381d9
commit 50c0104d57
227 changed files with 66687 additions and 20 deletions
+70
View File
@@ -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"))?)
}
+149
View File
@@ -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
View File
@@ -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
View File
@@ -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(&timestamp)?;
trace!(?encryption_type, "AS timestamp encryption params",);
let encrypted_timestamp = cipher.encrypt(key, PA_ENC_TIMESTAMP_KEY_USAGE, &timestamp_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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
}
}
+55
View File
@@ -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
View File
@@ -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
View File
@@ -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)?)
}
+876
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>;
+57
View File
@@ -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:?}"),
))
}
}
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
&params.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)
}