Skip to content

Commit 5b2b137

Browse files
fix(ntlm): preserve RC4 sealing state across mechListMIC (#753)
CredSSP can wrap `pubKeyAuth` after the initiator sends its mechListMIC but before it verifies the acceptor's MIC. Resetting both NTLM RC4 handles during verification loses the advanced send state and breaks the next wrapped message. Snapshot and restore only the sealing handle used for each MIC, including when verification fails. Remove the obsolete reset helper and add regression coverage for both directions, the CredSSP ordering, and invalid signatures. Sequence numbers remain unchanged. Tests: `cargo test -p sspi --lib ntlm::test:: --quiet` (36 passed); `cargo test -p sspi --features network_client,__test-data --test sspi ntlm --quiet` (14 passed); `cargo check -p sspi --quiet`; `cargo fmt --all --check`. Fixes: #752 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 809cd0a commit 5b2b137

2 files changed

Lines changed: 158 additions & 83 deletions

File tree

‎src/ntlm/mod.rs‎

Lines changed: 10 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,6 @@ pub struct Ntlm {
9999
// If the NTLM is used as server, then our_seq_number is the server sequence number and remote seq_number is the client sequence number.
100100
our_seq_number: u32,
101101
remote_seq_number: u32,
102-
// This flag is needed to correctly reset cipher state after MIC token generation/verification.
103-
is_client: bool,
104102

105103
session_key: Option<[u8; SESSION_KEY_SIZE]>,
106104
}
@@ -162,7 +160,6 @@ impl Ntlm {
162160

163161
our_seq_number: 0,
164162
remote_seq_number: 0,
165-
is_client: true,
166163
}
167164
}
168165

@@ -194,7 +191,6 @@ impl Ntlm {
194191

195192
our_seq_number: 0,
196193
remote_seq_number: 0,
197-
is_client: true,
198194
}
199195
}
200196

@@ -226,7 +222,6 @@ impl Ntlm {
226222

227223
our_seq_number: 0,
228224
remote_seq_number: 0,
229-
is_client: true,
230225
}
231226
}
232227

@@ -252,47 +247,6 @@ impl Ntlm {
252247
});
253248
}
254249

255-
/// Resets the cipher state.
256-
///
257-
/// According to the specification, we need to reset ciphers before and after MIC token generation/verification.
258-
/// [3.2.5.1 NTLM RC4 Key State for MechListMIC and First Signed Message](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/f38ae8e3-847d-4829-b933-5ac1911a00ba):
259-
/// > When NTLM is negotiated, the SPNG server MUST set OriginalHandle to ServerHandle before generating the mechListMIC,
260-
/// > then set ServerHandle to OriginalHandle after generating the mechListMIC. This results in the RC4 key state
261-
/// > being the same for the mechListMIC and for the first message signed by the application.
262-
/// >
263-
/// > Because the RC4 key state is the same for the mechListMIC and for the first message signed by the application,
264-
/// > the SPNEGO Extension server MUST set OriginalHandle to ClientHandle before validating the mechListMIC and then
265-
/// > set ClientHandle to OriginalHandle after validating the mechListMIC.
266-
fn reset_cipher_state(&mut self) -> crate::Result<()> {
267-
use crate::ntlm::messages::computations::generate_signing_key;
268-
use crate::ntlm::messages::{CLIENT_SEAL_MAGIC, CLIENT_SIGN_MAGIC, SERVER_SEAL_MAGIC, SERVER_SIGN_MAGIC};
269-
270-
let session_key = self.session_key.as_ref().ok_or_else(|| {
271-
Error::new(
272-
ErrorKind::OutOfSequence,
273-
"the session key is not established, cannot reset cipher state",
274-
)
275-
})?;
276-
277-
if self.is_client {
278-
self.send_signing_key = generate_signing_key(session_key.as_ref(), CLIENT_SIGN_MAGIC);
279-
self.recv_signing_key = generate_signing_key(session_key.as_ref(), SERVER_SIGN_MAGIC);
280-
self.send_sealing_key = Some(Rc4::new(
281-
generate_signing_key(session_key.as_ref(), CLIENT_SEAL_MAGIC).as_ref(),
282-
));
283-
self.recv_sealing_key = Some(Rc4::new(
284-
generate_signing_key(session_key.as_ref(), SERVER_SEAL_MAGIC).as_ref(),
285-
));
286-
} else {
287-
self.send_signing_key = generate_signing_key(session_key, SERVER_SIGN_MAGIC);
288-
self.recv_signing_key = generate_signing_key(session_key, CLIENT_SIGN_MAGIC);
289-
self.send_sealing_key = Some(Rc4::new(generate_signing_key(session_key, SERVER_SEAL_MAGIC).as_ref()));
290-
self.recv_sealing_key = Some(Rc4::new(generate_signing_key(session_key, CLIENT_SEAL_MAGIC).as_ref()));
291-
}
292-
293-
Ok(())
294-
}
295-
296250
/// Returns the next sequence number for outgoing messages and increments the internal counter.
297251
fn our_seq_num(&mut self) -> u32 {
298252
let seq_num = self.our_seq_number;
@@ -370,8 +324,6 @@ impl Ntlm {
370324
&mut self,
371325
builder: FilledAcceptSecurityContext<'_, <Self as SspiImpl>::CredentialsHandle>,
372326
) -> crate::Result<AcceptSecurityContextResult> {
373-
self.is_client = false;
374-
375327
let input = builder
376328
.input
377329
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified"))?;
@@ -417,8 +369,6 @@ impl Ntlm {
417369
&mut self,
418370
builder: &mut FilledInitializeSecurityContext<'_, '_, <Self as SspiImpl>::CredentialsHandle>,
419371
) -> crate::Result<InitializeSecurityContextResult> {
420-
self.is_client = true;
421-
422372
trace!(?builder);
423373

424374
let status = match self.state {
@@ -803,49 +753,24 @@ impl SspiEx for Ntlm {
803753
}
804754

805755
fn verify_mic_token(&mut self, signature: &[u8], data: &[u8], _: crate::private::Sealed) -> crate::Result<()> {
806-
// We reset the cipher state before and after MIC token verification.
807-
//
808-
// [3.2.5.1 NTLM RC4 Key State for MechListMIC and First Signed Message](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/f38ae8e3-847d-4829-b933-5ac1911a00ba):
809-
// > When NTLM is negotiated, the SPNG server MUST set OriginalHandle to ServerHandle before generating the mechListMIC,
810-
// > then set ServerHandle to OriginalHandle after generating the mechListMIC. This results in the RC4 key state
811-
// > being the same for the mechListMIC and for the first message signed by the application.
812-
// >
813-
// > Because the RC4 key state is the same for the mechListMIC and for the first message signed by the application,
814-
// > the SPNEGO Extension server MUST set OriginalHandle to ClientHandle before validating the mechListMIC and then
815-
// > set ClientHandle to OriginalHandle after validating the mechListMIC.
816-
817756
if self.recv_sealing_key.is_none() {
818757
self.complete_auth_token(&mut [])?;
819-
} else {
820-
self.reset_cipher_state()?;
821758
}
822759

823760
let seq_number = self.remote_seq_num();
824761

825762
let digest = compute_digest(self.recv_signing_key.as_ref(), seq_number, data)?;
826-
self.check_signature(seq_number, &digest, signature)?;
827-
828-
self.reset_cipher_state()?;
829763

830-
Ok(())
764+
// MS-SPNG 3.2.5.1 / 3.3.5.1: the MIC must not advance the application's RC4 handle.
765+
let original_recv_sealing_key = self.recv_sealing_key.clone();
766+
let result = self.check_signature(seq_number, &digest, signature);
767+
self.recv_sealing_key = original_recv_sealing_key;
768+
result
831769
}
832770

833771
fn generate_mic_token(&mut self, data: &[u8], _: crate::private::Sealed) -> crate::Result<Vec<u8>> {
834-
// We reset the cipher state before and after MIC token generation.
835-
//
836-
// [3.2.5.1 NTLM RC4 Key State for MechListMIC and First Signed Message](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/f38ae8e3-847d-4829-b933-5ac1911a00ba):
837-
// > When NTLM is negotiated, the SPNG server MUST set OriginalHandle to ServerHandle before generating the mechListMIC,
838-
// > then set ServerHandle to OriginalHandle after generating the mechListMIC. This results in the RC4 key state
839-
// > being the same for the mechListMIC and for the first message signed by the application.
840-
// >
841-
// > Because the RC4 key state is the same for the mechListMIC and for the first message signed by the application,
842-
// > the SPNEGO Extension server MUST set OriginalHandle to ClientHandle before validating the mechListMIC and then
843-
// > set ClientHandle to OriginalHandle after validating the mechListMIC.
844-
845772
if self.send_sealing_key.is_none() {
846773
self.complete_auth_token(&mut [])?;
847-
} else {
848-
self.reset_cipher_state()?;
849774
}
850775

851776
let seq_number = self.our_seq_num();
@@ -854,9 +779,11 @@ impl SspiEx for Ntlm {
854779

855780
let mut mic_token = vec![0; SIGNATURE_SIZE];
856781
let mut message = [SecurityBufferRef::token_buf(&mut mic_token)];
857-
self.compute_checksum(&mut message, seq_number, &digest)?;
858-
859-
self.reset_cipher_state()?;
782+
// MS-SPNG 3.2.5.1 / 3.3.5.1: restore only the handle used to sign the MIC.
783+
let original_send_sealing_key = self.send_sealing_key.clone();
784+
let result = self.compute_checksum(&mut message, seq_number, &digest);
785+
self.send_sealing_key = original_send_sealing_key;
786+
result?;
860787

861788
Ok(mic_token)
862789
}

‎src/ntlm/test.rs‎

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::crypto::{HASH_SIZE, Rc4};
2+
use crate::ntlm::messages::computations::generate_signing_key;
23
use crate::ntlm::messages::test::TEST_CREDENTIALS;
4+
use crate::ntlm::messages::{CLIENT_SEAL_MAGIC, CLIENT_SIGN_MAGIC, SERVER_SEAL_MAGIC, SERVER_SIGN_MAGIC};
35
use crate::ntlm::{
46
AuthenticateMessage, CHALLENGE_SIZE, ChallengeMessage, Mic, NegotiateFlags, NegotiateMessage, Ntlm, NtlmState,
57
SIGNATURE_SIZE,
@@ -331,6 +333,152 @@ fn verify_signature_fails_on_invalid_signature() {
331333
);
332334
}
333335

336+
fn mic_test_pair() -> (Ntlm, Ntlm) {
337+
let mut client = Ntlm::new();
338+
let mut server = Ntlm::new();
339+
client.flags = NegotiateFlags::NTLM_SSP_NEGOTIATE_KEY_EXCH;
340+
server.flags = NegotiateFlags::NTLM_SSP_NEGOTIATE_KEY_EXCH;
341+
client.session_key = Some(SEALING_KEY);
342+
server.session_key = Some(SEALING_KEY);
343+
344+
client.send_signing_key = generate_signing_key(&SEALING_KEY, CLIENT_SIGN_MAGIC);
345+
server.recv_signing_key = client.send_signing_key.clone();
346+
server.send_signing_key = generate_signing_key(&SEALING_KEY, SERVER_SIGN_MAGIC);
347+
client.recv_signing_key = server.send_signing_key.clone();
348+
349+
client.send_sealing_key = Some(Rc4::new(generate_signing_key(&SEALING_KEY, CLIENT_SEAL_MAGIC).as_ref()));
350+
server.recv_sealing_key = client.send_sealing_key.clone();
351+
server.send_sealing_key = Some(Rc4::new(generate_signing_key(&SEALING_KEY, SERVER_SEAL_MAGIC).as_ref()));
352+
client.recv_sealing_key = server.send_sealing_key.clone();
353+
354+
(client, server)
355+
}
356+
357+
fn assert_wrapped_message(sender: &mut Ntlm, receiver: &mut Ntlm, plaintext: &[u8]) {
358+
let mut token = [0; SIGNATURE_SIZE];
359+
let mut data = plaintext.to_vec();
360+
{
361+
let mut buffers = [
362+
SecurityBufferRef::token_buf(&mut token),
363+
SecurityBufferRef::data_buf(&mut data),
364+
];
365+
sender.encrypt_message(EncryptionFlags::empty(), &mut buffers).unwrap();
366+
assert_ne!(buffers[1].data(), plaintext);
367+
}
368+
369+
let mut buffers = [
370+
SecurityBufferRef::data_buf(&mut data),
371+
SecurityBufferRef::token_buf(&mut token),
372+
];
373+
receiver.decrypt_message(&mut buffers).unwrap();
374+
assert_eq!(buffers[0].data(), plaintext);
375+
}
376+
377+
#[test]
378+
fn generate_mic_token_preserves_both_sealing_handles() {
379+
let (client, server) = mic_test_pair();
380+
for mut context in [client, server] {
381+
let _ = context
382+
.send_sealing_key
383+
.as_mut()
384+
.unwrap()
385+
.process(b"earlier outgoing data");
386+
let _ = context
387+
.recv_sealing_key
388+
.as_mut()
389+
.unwrap()
390+
.process(b"earlier incoming data");
391+
let mut send_before = context.send_sealing_key.clone().unwrap();
392+
let mut recv_before = context.recv_sealing_key.clone().unwrap();
393+
394+
let mic = context.generate_mic_token(b"mech types", private::Sealed).unwrap();
395+
396+
assert_eq!(mic.len(), SIGNATURE_SIZE);
397+
assert_eq!(
398+
context.send_sealing_key.as_mut().unwrap().process(TEST_DATA),
399+
send_before.process(TEST_DATA)
400+
);
401+
assert_eq!(
402+
context.recv_sealing_key.as_mut().unwrap().process(TEST_DATA),
403+
recv_before.process(TEST_DATA)
404+
);
405+
}
406+
}
407+
408+
#[test]
409+
fn verify_mic_token_after_pub_key_auth_preserves_send_state() {
410+
let (mut client, mut server) = mic_test_pair();
411+
let mech_types = b"mech types";
412+
let server_mic = server.generate_mic_token(mech_types, private::Sealed).unwrap();
413+
414+
assert_wrapped_message(&mut client, &mut server, b"client pubKeyAuth");
415+
let mut reply_token = [0; SIGNATURE_SIZE];
416+
let mut reply_data = b"server pubKeyAuth".to_vec();
417+
server
418+
.encrypt_message(
419+
EncryptionFlags::empty(),
420+
&mut [
421+
SecurityBufferRef::token_buf(&mut reply_token),
422+
SecurityBufferRef::data_buf(&mut reply_data),
423+
],
424+
)
425+
.unwrap();
426+
427+
client
428+
.verify_mic_token(&server_mic, mech_types, private::Sealed)
429+
.unwrap();
430+
client
431+
.decrypt_message(&mut [
432+
SecurityBufferRef::data_buf(&mut reply_data),
433+
SecurityBufferRef::token_buf(&mut reply_token),
434+
])
435+
.unwrap();
436+
assert_eq!(reply_data, b"server pubKeyAuth");
437+
438+
assert_wrapped_message(&mut client, &mut server, b"following client payload");
439+
}
440+
441+
#[test]
442+
fn verify_mic_token_preserves_advanced_receive_state() {
443+
let (mut client, mut server) = mic_test_pair();
444+
assert_wrapped_message(&mut server, &mut client, b"earlier server payload");
445+
446+
let mech_types = b"mech types";
447+
let server_mic = server.generate_mic_token(mech_types, private::Sealed).unwrap();
448+
client
449+
.verify_mic_token(&server_mic, mech_types, private::Sealed)
450+
.unwrap();
451+
452+
assert_wrapped_message(&mut server, &mut client, b"following server payload");
453+
}
454+
455+
#[test]
456+
fn verify_mic_token_restores_receive_state_on_invalid_signature() {
457+
let (mut client, mut server) = mic_test_pair();
458+
assert_wrapped_message(&mut server, &mut client, b"earlier server payload");
459+
460+
let mut mic = server.generate_mic_token(b"mech types", private::Sealed).unwrap();
461+
mic[4] ^= 0xff;
462+
let mut recv_before = client.recv_sealing_key.clone().unwrap();
463+
let mut send_before = client.send_sealing_key.clone().unwrap();
464+
465+
assert_eq!(
466+
client
467+
.verify_mic_token(&mic, b"mech types", private::Sealed)
468+
.unwrap_err()
469+
.error_type,
470+
ErrorKind::MessageAltered
471+
);
472+
assert_eq!(
473+
client.recv_sealing_key.as_mut().unwrap().process(TEST_DATA),
474+
recv_before.process(TEST_DATA)
475+
);
476+
assert_eq!(
477+
client.send_sealing_key.as_mut().unwrap().process(TEST_DATA),
478+
send_before.process(TEST_DATA)
479+
);
480+
}
481+
334482
#[test]
335483
fn initialize_security_context_wrong_state_negotiate() {
336484
let mut context = Ntlm::new();

0 commit comments

Comments
 (0)