Skip to content

Commit be1b035

Browse files
authored
Fix NegotiateStream connections between Linux clients and Windows servers (#99909)
* Send the NegotiateSeal NTLM flag when client asked for ProtectionLevel.EncryptAndSign. Process the last handshake done message in NegotiateStream. In case of SPNEGO protocol it may contain message integrity check. Additionally, if the negotiated protocol is NTLM then we need to reset the encryption key after the message integrity check is verified. * Add test for the NegotiateSeal flag * Fix the test * Dummy commit * Fix the new _remoteOk logic in NegotiateStream to fire only when HandshakeComplete. If HandshakeComplete is not true, then the authentication blob will get processed with the normal flow. * Fix the value of NegotiateSeal in the final authentication message of Managed NTLM
1 parent ba84d1e commit be1b035

File tree

4 files changed

+71
-11
lines changed

4 files changed

+71
-11
lines changed

src/libraries/Common/tests/System/Net/Security/FakeNtlmServer.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public FakeNtlmServer(NetworkCredential expectedCredential)
4242
public bool IsAuthenticated { get; private set; }
4343
public bool IsMICPresent { get; private set; }
4444
public string? ClientSpecifiedSpn { get; private set; }
45+
public Flags InitialClientFlags { get; private set; }
46+
public Flags NegotiatedFlags => _negotiatedFlags;
4547

4648
private NetworkCredential _expectedCredential;
4749

@@ -83,7 +85,7 @@ private enum MessageType : uint
8385
}
8486

8587
[Flags]
86-
private enum Flags : uint
88+
public enum Flags : uint
8789
{
8890
NegotiateUnicode = 0x00000001,
8991
NegotiateOEM = 0x00000002,
@@ -177,17 +179,17 @@ private static ReadOnlySpan<byte> GetField(ReadOnlySpan<byte> payload, int field
177179
case MessageType.Negotiate:
178180
// We don't negotiate, we just verify
179181
Assert.True(incomingBlob.Length >= 32);
180-
Flags flags = (Flags)BinaryPrimitives.ReadUInt32LittleEndian(incomingBlob.AsSpan(12, 4));
181-
Assert.Equal(_requiredFlags, (flags & _requiredFlags));
182-
Assert.True((flags & (Flags.NegotiateOEM | Flags.NegotiateUnicode)) != 0);
183-
if (flags.HasFlag(Flags.NegotiateDomainSupplied))
182+
InitialClientFlags = (Flags)BinaryPrimitives.ReadUInt32LittleEndian(incomingBlob.AsSpan(12, 4));
183+
Assert.Equal(_requiredFlags, (InitialClientFlags & _requiredFlags));
184+
Assert.True((InitialClientFlags & (Flags.NegotiateOEM | Flags.NegotiateUnicode)) != 0);
185+
if (InitialClientFlags.HasFlag(Flags.NegotiateDomainSupplied))
184186
{
185187
string domain = Encoding.ASCII.GetString(GetField(incomingBlob, 16));
186188
Assert.Equal(_expectedCredential.Domain, domain);
187189
}
188190
_expectedMessageType = MessageType.Authenticate;
189191
_negotiateMessage = incomingBlob;
190-
return _challengeMessage = GenerateChallenge(flags);
192+
return _challengeMessage = GenerateChallenge(InitialClientFlags);
191193

192194
case MessageType.Authenticate:
193195
// Validate the authentication!

src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,14 @@ public override void Dispose()
270270
{
271271
Debug.Assert(incomingBlob.IsEmpty);
272272

273+
Flags requiredFlags = s_requiredFlags;
274+
if (_protectionLevel == ProtectionLevel.EncryptAndSign)
275+
{
276+
requiredFlags |= Flags.NegotiateSeal;
277+
}
278+
273279
_negotiateMessage = new byte[sizeof(NegotiateMessage)];
274-
CreateNtlmNegotiateMessage(_negotiateMessage);
280+
CreateNtlmNegotiateMessage(_negotiateMessage, requiredFlags);
275281

276282
outgoingBlob = _negotiateMessage;
277283
statusCode = NegotiateAuthenticationStatusCode.ContinueNeeded;
@@ -286,7 +292,7 @@ public override void Dispose()
286292
return outgoingBlob;
287293
}
288294

289-
private static unsafe void CreateNtlmNegotiateMessage(Span<byte> asBytes)
295+
private static unsafe void CreateNtlmNegotiateMessage(Span<byte> asBytes, Flags requiredFlags)
290296
{
291297
Debug.Assert(HeaderLength == NtlmHeader.Length);
292298
Debug.Assert(asBytes.Length == sizeof(NegotiateMessage));
@@ -296,7 +302,7 @@ private static unsafe void CreateNtlmNegotiateMessage(Span<byte> asBytes)
296302
asBytes.Clear();
297303
NtlmHeader.CopyTo(asBytes);
298304
message.Header.MessageType = MessageType.Negotiate;
299-
message.Flags = s_requiredFlags;
305+
message.Flags = requiredFlags;
300306
message.Version = s_version;
301307
}
302308

@@ -581,6 +587,13 @@ private static byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlyS
581587
return null;
582588
}
583589

590+
// We already negotiate signing, so we only need to check sealing/encryption.
591+
if ((flags & Flags.NegotiateSeal) == 0 && _protectionLevel == ProtectionLevel.EncryptAndSign)
592+
{
593+
statusCode = NegotiateAuthenticationStatusCode.QopNotSupported;
594+
return null;
595+
}
596+
584597
ReadOnlySpan<byte> targetInfo = GetField(challengeMessage.TargetInfo, blob);
585598
byte[] targetInfoBuffer = ProcessTargetInfo(targetInfo, out DateTime time, out bool hasNbNames);
586599

@@ -615,7 +628,7 @@ private static byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlyS
615628
NtlmHeader.CopyTo(responseAsSpan);
616629

617630
response.Header.MessageType = MessageType.Authenticate;
618-
response.Flags = s_requiredFlags;
631+
response.Flags = s_requiredFlags | (flags & Flags.NegotiateSeal);
619632
response.Version = s_version;
620633

621634
// Calculate hash for hmac - same for lm2 and ntlm2

src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,16 @@ private async Task ReceiveBlobAsync<TIOAdapter>(CancellationToken cancellationTo
883883

884884
if (_framer.ReadHeader.MessageId == FrameHeader.HandshakeDoneId)
885885
{
886-
_remoteOk = true;
886+
if (HandshakeComplete && message.Length > 0)
887+
{
888+
Debug.Assert(_context != null);
889+
_context.GetOutgoingBlob(message, out NegotiateAuthenticationStatusCode statusCode);
890+
_remoteOk = statusCode is NegotiateAuthenticationStatusCode.Completed;
891+
}
892+
else
893+
{
894+
_remoteOk = true;
895+
}
887896
}
888897
else if (_framer.ReadHeader.MessageId != FrameHeader.HandshakeId)
889898
{

src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,42 @@ public void NtlmIncorrectExchangeTest()
211211
Assert.False(fakeNtlmServer.IsAuthenticated);
212212
}
213213

214+
[ConditionalFact(nameof(IsNtlmAvailable))]
215+
public void NtlmEncryptionTest()
216+
{
217+
using FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight);
218+
219+
NegotiateAuthentication ntAuth = new NegotiateAuthentication(
220+
new NegotiateAuthenticationClientOptions
221+
{
222+
Package = "NTLM",
223+
Credential = s_testCredentialRight,
224+
TargetName = "HTTP/foo",
225+
RequiredProtectionLevel = ProtectionLevel.EncryptAndSign
226+
});
227+
228+
NegotiateAuthenticationStatusCode statusCode;
229+
byte[]? negotiateBlob = ntAuth.GetOutgoingBlob((byte[])null, out statusCode);
230+
Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode);
231+
Assert.NotNull(negotiateBlob);
232+
233+
byte[]? challengeBlob = fakeNtlmServer.GetOutgoingBlob(negotiateBlob);
234+
Assert.NotNull(challengeBlob);
235+
// Validate that the client sent NegotiateSeal flag
236+
Assert.Equal(FakeNtlmServer.Flags.NegotiateSeal, (fakeNtlmServer.InitialClientFlags & FakeNtlmServer.Flags.NegotiateSeal));
237+
238+
byte[]? authenticateBlob = ntAuth.GetOutgoingBlob(challengeBlob, out statusCode);
239+
Assert.Equal(NegotiateAuthenticationStatusCode.Completed, statusCode);
240+
Assert.NotNull(authenticateBlob);
241+
242+
byte[]? empty = fakeNtlmServer.GetOutgoingBlob(authenticateBlob);
243+
Assert.Null(empty);
244+
Assert.True(fakeNtlmServer.IsAuthenticated);
245+
246+
// Validate that the NegotiateSeal flag survived the full exchange
247+
Assert.Equal(FakeNtlmServer.Flags.NegotiateSeal, (fakeNtlmServer.NegotiatedFlags & FakeNtlmServer.Flags.NegotiateSeal));
248+
}
249+
214250
[ConditionalFact(nameof(IsNtlmAvailable))]
215251
public void NtlmSignatureTest()
216252
{

0 commit comments

Comments
 (0)