1pub(crate) mod crypto;
8pub(crate) mod messages;
9
10pub(crate) use messages::{
12 create_authenticate_message_with_cbt_and_key, create_authenticate_message_with_key_and_mic,
13 create_negotiate_message, decode_challenge_header, encode_authorization,
14};
15#[cfg(any(feature = "credssp", feature = "__internal"))]
19#[allow(unreachable_pub)]
20pub use messages::parse_challenge;
22#[cfg(feature = "credssp")]
23#[allow(unreachable_pub)] pub use messages::{create_authenticate_message_credssp, create_negotiate_message_credssp};
25
26use crate::error::NtlmError;
28use crypto::{Rc4State, hmac_md5};
29
30pub struct NtlmSession {
51 client_sign_key: [u8; 16],
52 #[allow(dead_code)] server_sign_key: [u8; 16],
54 client_seq_num: u32,
55 server_seq_num: u32,
56 client_seal_handle: Rc4State,
57 server_seal_handle: Rc4State,
58}
59
60impl NtlmSession {
61 pub fn from_auth(exported_session_key: &[u8; 16]) -> Self {
67 let client_seal_key = Self::derive_key(
68 exported_session_key,
69 b"session key to client-to-server sealing key magic constant\0",
70 );
71 let client_sign_key = Self::derive_key(
72 exported_session_key,
73 b"session key to client-to-server signing key magic constant\0",
74 );
75 let server_seal_key = Self::derive_key(
76 exported_session_key,
77 b"session key to server-to-client sealing key magic constant\0",
78 );
79 let server_sign_key = Self::derive_key(
80 exported_session_key,
81 b"session key to server-to-client signing key magic constant\0",
82 );
83
84 Self {
85 client_sign_key,
86 server_sign_key,
87 client_seq_num: 0,
88 server_seq_num: 0,
89 client_seal_handle: Rc4State::new(&client_seal_key),
90 server_seal_handle: Rc4State::new(&server_seal_key),
91 }
92 }
93
94 fn derive_key(session_key: &[u8; 16], magic: &[u8]) -> [u8; 16] {
95 use md5::Digest;
96 let mut hasher = md5::Md5::new();
97 hasher.update(session_key);
98 hasher.update(magic);
99 let result = hasher.finalize();
100 let mut key = [0u8; 16];
101 key.copy_from_slice(&result);
102 key
103 }
104
105 pub fn seal(&mut self, plaintext: &[u8]) -> Vec<u8> {
112 let mut sig_input = Vec::with_capacity(4 + plaintext.len());
114 sig_input.extend_from_slice(&self.client_seq_num.to_le_bytes());
115 sig_input.extend_from_slice(plaintext);
116 let checksum = hmac_md5(&self.client_sign_key, &sig_input);
117 let mut checksum_8 = [0u8; 8];
118 checksum_8.copy_from_slice(&checksum[..8]);
119
120 let mut ciphertext = plaintext.to_vec();
123 self.client_seal_handle.process(&mut ciphertext);
124
125 self.client_seal_handle.process(&mut checksum_8);
127
128 let mut result = Vec::with_capacity(16 + ciphertext.len());
130 result.extend_from_slice(&1u32.to_le_bytes()); result.extend_from_slice(&checksum_8);
132 result.extend_from_slice(&self.client_seq_num.to_le_bytes());
133
134 self.client_seq_num += 1;
135
136 result.extend_from_slice(&ciphertext);
137 result
138 }
139
140 pub fn sign(&mut self, data: &[u8]) -> [u8; 16] {
145 let mut sig_input = Vec::with_capacity(4 + data.len());
146 sig_input.extend_from_slice(&self.client_seq_num.to_le_bytes());
147 sig_input.extend_from_slice(data);
148 let checksum = hmac_md5(&self.client_sign_key, &sig_input);
149 let mut checksum_8 = [0u8; 8];
150 checksum_8.copy_from_slice(&checksum[..8]);
151 self.client_seal_handle.process(&mut checksum_8);
153
154 let mut sig = [0u8; 16];
155 sig[0..4].copy_from_slice(&1u32.to_le_bytes());
156 sig[4..12].copy_from_slice(&checksum_8);
157 sig[12..16].copy_from_slice(&self.client_seq_num.to_le_bytes());
158 self.client_seq_num += 1;
159 sig
160 }
161
162 pub fn unseal(&mut self, sealed: &[u8]) -> Result<Vec<u8>, NtlmError> {
176 if sealed.len() < 16 {
177 return Err(NtlmError::InvalidMessage("sealed message too short".into()));
178 }
179
180 let signature = &sealed[..16];
181 let ciphertext = &sealed[16..];
182
183 let version = u32::from_le_bytes([signature[0], signature[1], signature[2], signature[3]]);
185 if version != 1 {
186 return Err(NtlmError::InvalidMessage("bad signature version".into()));
187 }
188
189 let sig_seq =
191 u32::from_le_bytes([signature[12], signature[13], signature[14], signature[15]]);
192 if sig_seq != self.server_seq_num {
193 return Err(NtlmError::InvalidMessage("sequence number mismatch".into()));
194 }
195
196 let mut plaintext = ciphertext.to_vec();
198 self.server_seal_handle.process(&mut plaintext);
199
200 let mut sig_checksum = [0u8; 8];
202 sig_checksum.copy_from_slice(&signature[4..12]);
203 self.server_seal_handle.process(&mut sig_checksum);
204
205 let mut expected_sig_input = Vec::with_capacity(4 + plaintext.len());
207 expected_sig_input.extend_from_slice(&self.server_seq_num.to_le_bytes());
208 expected_sig_input.extend_from_slice(&plaintext);
209 let expected_checksum = hmac_md5(&self.server_sign_key, &expected_sig_input);
210 if sig_checksum != expected_checksum[..8] {
211 return Err(NtlmError::InvalidMessage("checksum mismatch".into()));
212 }
213
214 self.server_seq_num += 1;
215 Ok(plaintext)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn unhex(s: &str) -> Vec<u8> {
224 (0..s.len())
225 .step_by(2)
226 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
227 .collect()
228 }
229
230 #[test]
231 fn mic_hmac_md5_matches_pywinrm_vector() {
232 let session_key = unhex("101112131415161718191a1b1c1d1e1f");
234 let neg = unhex(
235 "4e544c4d5353500001000000378208e200000000280000000000000028000000000c01000000000f",
236 );
237 let chal = unhex(
238 "4e544c4d53535000020000001e001e003800000035828ae28dc091106adfffd0000000000000000098009800560000000a00f4650000000f570049004e002d00540054005300540041004e005500510030003800530002001e00570049004e002d00540054005300540041004e005500510030003800530001001e00570049004e002d00540054005300540041004e005500510030003800530004001e00570049004e002d00540054005300540041004e005500510030003800530003001e00570049004e002d00540054005300540041004e00550051003000380053000700080000f8c4ba9dc7dc0100000000",
239 );
240 let auth = unhex(
241 "4e544c4d53535000030000001800180058000000f600f6007000000000000000660100000e000e00660100001e001e0074010000100010009201000035828ae2000c01000000000f000000000000000000000000000000000000000000000000000000000000000000000000000000008d3613113b1608b1afb92a5f0eb02477010100000000000000f8c4ba9dc7dc0120212223242526270000000002001e00570049004e002d00540054005300540041004e005500510030003800530001001e00570049004e002d00540054005300540041004e005500510030003800530004001e00570049004e002d00540054005300540041004e005500510030003800530003001e00570049004e002d00540054005300540041004e00550051003000380053000700080000f8c4ba9dc7dc010900220048005400540050002f003100390032002e003100360038002e00390036002e00310006000400020000000000000000000000760061006700720061006e00740050004f005300540045002d0046004900580045002d004c004f004900430017dc1c37061dbbea1b965421d8311908",
242 );
243 let expected = unhex("a235369e56d2a0fad48a755b6b4c63e6");
244 let mut input = Vec::new();
245 input.extend_from_slice(&neg);
246 input.extend_from_slice(&chal);
247 input.extend_from_slice(&auth);
248 let key: [u8; 16] = session_key.try_into().unwrap();
249 let mic = crypto::hmac_md5(&key, &input);
250 assert_eq!(mic.to_vec(), expected, "MIC mismatch");
251 }
252
253 #[test]
254 fn ntlm_session_keys_match_pywinrm_vector() {
255 let key: [u8; 16] = [
257 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
258 0x1e, 0x1f,
259 ];
260 let cli_seal = NtlmSession::derive_key(
261 &key,
262 b"session key to client-to-server sealing key magic constant\0",
263 );
264 let srv_seal = NtlmSession::derive_key(
265 &key,
266 b"session key to server-to-client sealing key magic constant\0",
267 );
268 let cli_sign = NtlmSession::derive_key(
269 &key,
270 b"session key to client-to-server signing key magic constant\0",
271 );
272 let srv_sign = NtlmSession::derive_key(
273 &key,
274 b"session key to server-to-client signing key magic constant\0",
275 );
276 let h = |b: &[u8]| b.iter().map(|x| format!("{:02x}", x)).collect::<String>();
277 assert_eq!(
278 h(&cli_seal),
279 "af22a2127a4b090cccdfa26c427969c7",
280 "client seal key"
281 );
282 assert_eq!(
283 h(&srv_seal),
284 "b9e4af6ccd5f5edeb067d13815036db5",
285 "server seal key"
286 );
287 assert_eq!(
288 h(&cli_sign),
289 "a14c3d1e1b365279873f7dcf51aed29d",
290 "client sign key"
291 );
292 assert_eq!(
293 h(&srv_sign),
294 "dbfeaa5883b889757ff1d849f31d6d53",
295 "server sign key"
296 );
297 }
298
299 #[test]
300 fn sign_matches_pyspnego_mech_list_mic() {
301 let key: [u8; 16] = [
302 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
303 0x1e, 0x1f,
304 ];
305 let mut s = NtlmSession::from_auth(&key);
306 let mech = unhex("300c060a2b06010401823702020a");
307 let sig = s.sign(&mech);
308 assert_eq!(
309 sig.to_vec(),
310 unhex("0100000002f81117bb3953f700000000"),
311 "mechListMIC mismatch"
312 );
313 }
314
315 #[test]
316 fn seal_matches_pywinrm_vector() {
317 let key: [u8; 16] = [
318 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
319 0x1e, 0x1f,
320 ];
321 let mut session = NtlmSession::from_auth(&key);
322 let plaintext: Vec<u8> = (0u8..32).collect();
323 let sealed = session.seal(&plaintext);
324 let expected = unhex(
325 "010000000f62d40713d158d4000000001b23173031109ef42a884e223417c37909fada44f3180048ab67dc2d64ea9c41",
326 );
327 assert_eq!(sealed, expected, "seal output mismatch");
328 }
329
330 #[test]
331 fn ntlm_session_seal_unseal_roundtrip() {
332 let session_key: [u8; 16] = [
333 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
334 0x0F, 0x10,
335 ];
336
337 let mut session = NtlmSession::from_auth(&session_key);
338 let plaintext = b"Hello, WinRM! This is a test SOAP message.";
339 let sealed = session.seal(plaintext);
340
341 assert_eq!(sealed.len(), 16 + plaintext.len());
342
343 let version = u32::from_le_bytes(sealed[0..4].try_into().unwrap());
344 assert_eq!(version, 1);
345
346 let seq_num = u32::from_le_bytes(sealed[12..16].try_into().unwrap());
347 assert_eq!(seq_num, 0);
348
349 assert_ne!(&sealed[16..], &plaintext[..]);
350 }
351
352 #[test]
353 fn ntlm_session_seal_increments_sequence() {
354 let session_key: [u8; 16] = [0xAA; 16];
355 let mut session = NtlmSession::from_auth(&session_key);
356
357 let sealed1 = session.seal(b"message 1");
358 let sealed2 = session.seal(b"message 2");
359
360 let seq1 = u32::from_le_bytes(sealed1[12..16].try_into().unwrap());
361 let seq2 = u32::from_le_bytes(sealed2[12..16].try_into().unwrap());
362 assert_eq!(seq1, 0);
363 assert_eq!(seq2, 1);
364 }
365
366 #[test]
367 fn ntlm_session_unseal_too_short() {
368 let session_key: [u8; 16] = [0xBB; 16];
369 let mut session = NtlmSession::from_auth(&session_key);
370 let result = session.unseal(&[0u8; 10]);
371 assert!(result.is_err());
372 let err = format!("{}", result.unwrap_err());
373 assert!(err.contains("too short"));
374 }
375
376 #[test]
377 fn ntlm_session_unseal_bad_version() {
378 let session_key: [u8; 16] = [0xCC; 16];
379 let mut session = NtlmSession::from_auth(&session_key);
380 let mut fake = vec![0u8; 32];
381 fake[0..4].copy_from_slice(&2u32.to_le_bytes());
382 fake[12..16].copy_from_slice(&0u32.to_le_bytes());
383 let result = session.unseal(&fake);
384 assert!(result.is_err());
385 let err = format!("{}", result.unwrap_err());
386 assert!(err.contains("bad signature version"));
387 }
388
389 #[test]
390 fn ntlm_session_unseal_bad_sequence() {
391 let session_key: [u8; 16] = [0xDD; 16];
392 let mut session = NtlmSession::from_auth(&session_key);
393 let mut fake = vec![0u8; 32];
394 fake[0..4].copy_from_slice(&1u32.to_le_bytes());
395 fake[12..16].copy_from_slice(&99u32.to_le_bytes());
396 let result = session.unseal(&fake);
397 assert!(result.is_err());
398 let err = format!("{}", result.unwrap_err());
399 assert!(err.contains("sequence number mismatch"));
400 }
401
402 #[test]
403 fn ntlm_session_seal_unseal_symmetric() {
404 let session_key: [u8; 16] = [
405 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0,
406 0xF0, 0x00,
407 ];
408
409 let mut client = NtlmSession::from_auth(&session_key);
410 let plaintext = b"SOAP envelope data for WinRM";
411 let sealed = client.seal(plaintext);
412
413 assert!(sealed.len() > 16);
414 let version = u32::from_le_bytes(sealed[0..4].try_into().unwrap());
415 assert_eq!(version, 1);
416
417 let mut client2 = NtlmSession::from_auth(&session_key);
418 let sealed2 = client2.seal(plaintext);
419 assert_eq!(sealed, sealed2);
420 }
421
422 #[test]
423 fn ntlm_session_unseal_exact_16_bytes() {
424 let session_key: [u8; 16] = [0xEE; 16];
431 let mut session = NtlmSession::from_auth(&session_key);
432 let mut msg = vec![0u8; 16];
433 msg[0..4].copy_from_slice(&1u32.to_le_bytes());
434 msg[12..16].copy_from_slice(&0u32.to_le_bytes());
435 let result = session.unseal(&msg);
436 assert!(result.is_err(), "fake checksum should be rejected");
437 let err = format!("{}", result.unwrap_err());
438 assert!(err.contains("checksum mismatch"));
439 }
440
441 #[test]
442 fn ntlm_session_derive_key_is_deterministic() {
443 let key1 = [0x42u8; 16];
444 let key2 = [0x42u8; 16];
445 let mut s1 = NtlmSession::from_auth(&key1);
446 let mut s2 = NtlmSession::from_auth(&key2);
447
448 let sealed1 = s1.seal(b"test");
449 let sealed2 = s2.seal(b"test");
450 assert_eq!(
451 sealed1, sealed2,
452 "same key must produce identical sealed output"
453 );
454
455 let mut s3 = NtlmSession::from_auth(&[0u8; 16]);
456 let sealed3 = s3.seal(b"test");
457 assert_ne!(
458 sealed1, sealed3,
459 "different keys must produce different sealed output"
460 );
461
462 let mut s4 = NtlmSession::from_auth(&[1u8; 16]);
463 let sealed4 = s4.seal(b"test");
464 assert_ne!(
465 sealed1, sealed4,
466 "different keys must produce different sealed output"
467 );
468 }
469
470 #[test]
471 fn ntlm_session_multiple_seal_sequence_numbers() {
472 let key = [0xAA; 16];
473 let mut sealer = NtlmSession::from_auth(&key);
474
475 let msg1 = sealer.seal(b"first");
476 let msg2 = sealer.seal(b"second");
477
478 let seq1 = u32::from_le_bytes(msg1[12..16].try_into().unwrap());
479 let seq2 = u32::from_le_bytes(msg2[12..16].try_into().unwrap());
480 assert_eq!(seq1, 0);
481 assert_eq!(seq2, 1);
482 }
483
484 #[test]
485 fn ntlm_session_unseal_rejects_stale_sequence() {
486 let key = [0xAA; 16];
487 let mut session = NtlmSession::from_auth(&key);
488 let mut fake = vec![0u8; 20];
490 fake[0..4].copy_from_slice(&1u32.to_le_bytes());
491 fake[12..16].copy_from_slice(&99u32.to_le_bytes());
492 let result = session.unseal(&fake);
493 assert!(result.is_err(), "stale seq_num should be rejected");
494 }
495
496 #[test]
497 fn ntlm_session_unseal_rejects_tampered_checksum() {
498 let key = [0xBB; 16];
499 let mut session = NtlmSession::from_auth(&key);
500 let mut fake = vec![0u8; 32];
502 fake[0..4].copy_from_slice(&1u32.to_le_bytes());
503 fake[4..12].copy_from_slice(&[0xFF; 8]); fake[12..16].copy_from_slice(&0u32.to_le_bytes());
505 let result = session.unseal(&fake);
506 assert!(result.is_err(), "tampered checksum should be rejected");
507 let err = format!("{}", result.unwrap_err());
508 assert!(err.contains("checksum mismatch"));
509 }
510
511 #[test]
515 fn sign_increments_sequence_number() {
516 let key = [0xDD; 16];
517 let mut session = NtlmSession::from_auth(&key);
518 let sig1 = session.sign(b"msg1");
519 let sig2 = session.sign(b"msg1"); let seq1 = u32::from_le_bytes(sig1[12..16].try_into().unwrap());
521 let seq2 = u32::from_le_bytes(sig2[12..16].try_into().unwrap());
522 assert_eq!(seq1, 0);
523 assert_eq!(seq2, 1);
524 }
525
526 }