Skip to content

Commit b040b7a

Browse files
rzikmfilipnavarawfurt
authored
[release/8.0] Fix NegotiateStream connections between Linux clients and Windows servers (#102216)
* 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 * Fix build * Remove unwanted test change --------- Co-authored-by: Filip Navara <[email protected]> Co-authored-by: wfurt <[email protected]>
1 parent d2f465a commit b040b7a

File tree

4 files changed

+72
-12
lines changed

4 files changed

+72
-12
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
@@ -262,8 +262,14 @@ public override void Dispose()
262262
{
263263
Debug.Assert(incomingBlob.IsEmpty);
264264

265+
Flags requiredFlags = s_requiredFlags;
266+
if (_protectionLevel == ProtectionLevel.EncryptAndSign)
267+
{
268+
requiredFlags |= Flags.NegotiateSeal;
269+
}
270+
265271
_negotiateMessage = new byte[sizeof(NegotiateMessage)];
266-
CreateNtlmNegotiateMessage(_negotiateMessage);
272+
CreateNtlmNegotiateMessage(_negotiateMessage, requiredFlags);
267273

268274
outgoingBlob = _negotiateMessage;
269275
statusCode = NegotiateAuthenticationStatusCode.ContinueNeeded;
@@ -278,7 +284,7 @@ public override void Dispose()
278284
return outgoingBlob;
279285
}
280286

281-
private static unsafe void CreateNtlmNegotiateMessage(Span<byte> asBytes)
287+
private static unsafe void CreateNtlmNegotiateMessage(Span<byte> asBytes, Flags requiredFlags)
282288
{
283289
Debug.Assert(HeaderLength == NtlmHeader.Length);
284290
Debug.Assert(asBytes.Length == sizeof(NegotiateMessage));
@@ -288,7 +294,7 @@ private static unsafe void CreateNtlmNegotiateMessage(Span<byte> asBytes)
288294
asBytes.Clear();
289295
NtlmHeader.CopyTo(asBytes);
290296
message.Header.MessageType = MessageType.Negotiate;
291-
message.Flags = s_requiredFlags;
297+
message.Flags = requiredFlags;
292298
message.Version = s_version;
293299
}
294300

@@ -573,6 +579,13 @@ private static byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlyS
573579
return null;
574580
}
575581

582+
// We already negotiate signing, so we only need to check sealing/encryption.
583+
if ((flags & Flags.NegotiateSeal) == 0 && _protectionLevel == ProtectionLevel.EncryptAndSign)
584+
{
585+
statusCode = NegotiateAuthenticationStatusCode.QopNotSupported;
586+
return null;
587+
}
588+
576589
ReadOnlySpan<byte> targetInfo = GetField(challengeMessage.TargetInfo, blob);
577590
byte[] targetInfoBuffer = ProcessTargetInfo(targetInfo, out DateTime time, out bool hasNbNames);
578591

@@ -607,7 +620,7 @@ private static byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlyS
607620
NtlmHeader.CopyTo(responseAsSpan);
608621

609622
response.Header.MessageType = MessageType.Authenticate;
610-
response.Flags = s_requiredFlags;
623+
response.Flags = s_requiredFlags | (flags & Flags.NegotiateSeal);
611624
response.Version = s_version;
612625

613626
// 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: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,42 @@ public void NtlmIncorrectExchangeTest()
190190
Assert.False(fakeNtlmServer.IsAuthenticated);
191191
}
192192

193+
[ConditionalFact(nameof(IsNtlmAvailable))]
194+
public void NtlmEncryptionTest()
195+
{
196+
using FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight);
197+
198+
NegotiateAuthentication ntAuth = new NegotiateAuthentication(
199+
new NegotiateAuthenticationClientOptions
200+
{
201+
Package = "NTLM",
202+
Credential = s_testCredentialRight,
203+
TargetName = "HTTP/foo",
204+
RequiredProtectionLevel = ProtectionLevel.EncryptAndSign
205+
});
206+
207+
NegotiateAuthenticationStatusCode statusCode;
208+
byte[]? negotiateBlob = ntAuth.GetOutgoingBlob((byte[])null, out statusCode);
209+
Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode);
210+
Assert.NotNull(negotiateBlob);
211+
212+
byte[]? challengeBlob = fakeNtlmServer.GetOutgoingBlob(negotiateBlob);
213+
Assert.NotNull(challengeBlob);
214+
// Validate that the client sent NegotiateSeal flag
215+
Assert.Equal(FakeNtlmServer.Flags.NegotiateSeal, (fakeNtlmServer.InitialClientFlags & FakeNtlmServer.Flags.NegotiateSeal));
216+
217+
byte[]? authenticateBlob = ntAuth.GetOutgoingBlob(challengeBlob, out statusCode);
218+
Assert.Equal(NegotiateAuthenticationStatusCode.Completed, statusCode);
219+
Assert.NotNull(authenticateBlob);
220+
221+
byte[]? empty = fakeNtlmServer.GetOutgoingBlob(authenticateBlob);
222+
Assert.Null(empty);
223+
Assert.True(fakeNtlmServer.IsAuthenticated);
224+
225+
// Validate that the NegotiateSeal flag survived the full exchange
226+
Assert.Equal(FakeNtlmServer.Flags.NegotiateSeal, (fakeNtlmServer.NegotiatedFlags & FakeNtlmServer.Flags.NegotiateSeal));
227+
}
228+
193229
[ConditionalFact(nameof(IsNtlmAvailable))]
194230
[ActiveIssue("https://github.com/dotnet/runtime/issues/65678", TestPlatforms.OSX | TestPlatforms.iOS | TestPlatforms.MacCatalyst)]
195231
public void NtlmSignatureTest()
@@ -218,7 +254,7 @@ public void NtlmSignatureTest()
218254
fakeNtlmServer.Unwrap(output.WrittenSpan, temp);
219255
Assert.Equal(s_Hello, temp);
220256

221-
// Test creating signature on server side and decoding it with VerifySignature on client side
257+
// Test creating signature on server side and decoding it with VerifySignature on client side
222258
byte[] serverSignedMessage = new byte[16 + s_Hello.Length];
223259
fakeNtlmServer.Wrap(s_Hello, serverSignedMessage);
224260
output.Clear();

0 commit comments

Comments
 (0)