Skip to content

Commit 2e2635d

Browse files
Shubham-0-0-7claude
andcommitted
fix(kerberos): keep secrets out of trace logs
The password-change request recorded the new password as a span field, and logged the KRB_PRIV sub-session key and plaintext, which contains the new password. The authenticator, which carries the sub-session key, was also recorded by `generate_ap_req`, both as a span field and as plaintext. Skip the span fields of both functions and log only the ciphertext and the encryption type. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 1cd2feb commit 2e2635d

1 file changed

Lines changed: 102 additions & 13 deletions

File tree

‎src/kerberos/client/generators.rs‎

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ pub fn generate_tgs_ap_req(
803803
}))
804804
}
805805

806-
#[instrument(level = "trace", ret)]
806+
#[instrument(level = "trace", ret, skip_all)]
807807
pub fn generate_ap_req(
808808
ticket: Ticket,
809809
session_key: &Secret<Vec<u8>>,
@@ -817,11 +817,7 @@ pub fn generate_ap_req(
817817
let encoded_authenticator = picky_asn1_der::to_vec(&authenticator)?;
818818
let encrypted_authenticator = cipher.encrypt(session_key.as_ref(), AP_REQ_AUTHENTICATOR, &encoded_authenticator)?;
819819

820-
trace!(
821-
plain = ?encoded_authenticator,
822-
encrypted = ?encrypted_authenticator,
823-
"AP_REQ authenticator",
824-
);
820+
trace!(encrypted = ?encrypted_authenticator, "AP_REQ authenticator");
825821

826822
Ok(ApReq::from(ApReqInner {
827823
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
@@ -843,7 +839,7 @@ pub fn get_mech_list() -> MechTypeList {
843839
MechTypeList::from(vec![MechType::from(oids::ms_krb5()), MechType::from(oids::krb5())])
844840
}
845841

846-
#[instrument(level = "trace", ret)]
842+
#[instrument(level = "trace", ret, skip_all)]
847843
pub fn generate_krb_priv_request(
848844
ticket: Ticket,
849845
session_key: &Secret<Vec<u8>>,
@@ -877,12 +873,8 @@ pub fn generate_krb_priv_request(
877873

878874
let enc_part = cipher.encrypt(encryption_key, KRB_PRIV_ENC_PART, &encoded_krb_priv)?;
879875

880-
trace!(?encryption_key, ?encryption_type, "KRB_PRIV encryption params",);
881-
trace!(
882-
plain = ?encoded_krb_priv,
883-
encrypted = ?enc_part,
884-
"KRB_PRIV encrypted part",
885-
);
876+
trace!(?encryption_type, "KRB_PRIV encryption params",);
877+
trace!(encrypted = ?enc_part, "KRB_PRIV encrypted part");
886878

887879
let krb_priv = KrbPriv::from(KrbPrivInner {
888880
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
@@ -903,6 +895,7 @@ pub fn generate_krb_priv_request(
903895
#[cfg(test)]
904896
mod tests {
905897
use picky_krb::constants::types::NT_PRINCIPAL;
898+
use picky_krb::data_types::TicketInner;
906899

907900
use super::*;
908901

@@ -1106,4 +1099,100 @@ mod tests {
11061099
);
11071100
assert_eq!(decoded.pausec.0.unwrap().0.0, 123_000_u32.to_be_bytes());
11081101
}
1102+
1103+
#[derive(Clone, Default)]
1104+
struct CapturedLogs(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
1105+
1106+
impl std::io::Write for CapturedLogs {
1107+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1108+
self.0.lock().unwrap().extend_from_slice(buf);
1109+
Ok(buf.len())
1110+
}
1111+
1112+
fn flush(&mut self) -> std::io::Result<()> {
1113+
Ok(())
1114+
}
1115+
}
1116+
1117+
fn krb_string(value: &str) -> KerberosStringAsn1 {
1118+
KerberosStringAsn1::from(IA5String::from_string(value.to_owned()).unwrap())
1119+
}
1120+
1121+
fn principal() -> PrincipalName {
1122+
PrincipalName {
1123+
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_PRINCIPAL])),
1124+
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![krb_string("alice")])),
1125+
}
1126+
}
1127+
1128+
#[test]
1129+
fn krb_priv_request_does_not_trace_new_password_or_subkey() {
1130+
let new_password = b"hunter2-new-password";
1131+
let sub_key = vec![0xA5_u8; 32];
1132+
let session_key = Secret::from(vec![0x5A_u8; 32]);
1133+
1134+
let ticket = Ticket::from(TicketInner {
1135+
tkt_vno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
1136+
realm: ExplicitContextTag1::from(krb_string("EXAMPLE.COM")),
1137+
sname: ExplicitContextTag2::from(principal()),
1138+
enc_part: ExplicitContextTag3::from(EncryptedData {
1139+
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![18])),
1140+
kvno: Optional::from(None),
1141+
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(vec![0x11; 16])),
1142+
}),
1143+
});
1144+
let authenticator = Authenticator::from(AuthenticatorInner {
1145+
authenticator_vno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
1146+
crealm: ExplicitContextTag1::from(krb_string("EXAMPLE.COM")),
1147+
cname: ExplicitContextTag2::from(principal()),
1148+
cksum: Optional::from(None),
1149+
cusec: ExplicitContextTag4::from(IntegerAsn1::from(vec![0])),
1150+
ctime: ExplicitContextTag5::from(KerberosTime::from(GeneralizedTime::from(
1151+
OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(),
1152+
))),
1153+
subkey: Optional::from(Some(ExplicitContextTag6::from(EncryptionKey {
1154+
key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![18])),
1155+
key_value: ExplicitContextTag1::from(OctetStringAsn1::from(sub_key.clone())),
1156+
}))),
1157+
seq_number: Optional::from(None),
1158+
authorization_data: Optional::from(None),
1159+
});
1160+
let enc_params = EncryptionParams {
1161+
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
1162+
..EncryptionParams::default_for_client()
1163+
};
1164+
1165+
let logs = CapturedLogs::default();
1166+
let subscriber = tracing_subscriber::fmt()
1167+
.with_max_level(tracing::Level::TRACE)
1168+
.with_ansi(false)
1169+
.with_writer({
1170+
let logs = logs.clone();
1171+
move || logs.clone()
1172+
})
1173+
.finish();
1174+
tracing::subscriber::with_default(subscriber, || {
1175+
generate_krb_priv_request(
1176+
ticket,
1177+
&session_key,
1178+
new_password,
1179+
&authenticator,
1180+
&enc_params,
1181+
1,
1182+
"HOST",
1183+
)
1184+
.expect("generate KRB_PRIV request");
1185+
});
1186+
1187+
let output = String::from_utf8(logs.0.lock().unwrap().clone()).unwrap();
1188+
assert!(!output.is_empty(), "the trace subscriber captured nothing");
1189+
// Byte slices are rendered by `Debug` as `[a, b, ...]` and ASN.1 octet strings as `0xabcd...`.
1190+
for (name, secret) in [("new password", &new_password[..]), ("sub-session key", &sub_key[..])] {
1191+
let list = format!("{secret:?}");
1192+
let hex: String = secret.iter().map(|byte| format!("{byte:02x}")).collect();
1193+
for rendered in [list, hex] {
1194+
assert!(!output.contains(&rendered), "trace output leaks the {name}: {output}");
1195+
}
1196+
}
1197+
}
11091198
}

0 commit comments

Comments
 (0)