From be1b0359f018ea1473408f7b153bf6acf8ab9917 Mon Sep 17 00:00:00 2001 From: Filip Navara Date: Tue, 26 Mar 2024 17:04:56 +0100 Subject: [PATCH] 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 --- .../System/Net/Security/FakeNtlmServer.cs | 14 ++++---- .../NegotiateAuthenticationPal.ManagedNtlm.cs | 21 ++++++++--- .../System/Net/Security/NegotiateStream.cs | 11 +++++- .../UnitTests/NegotiateAuthenticationTests.cs | 36 +++++++++++++++++++ 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Security/FakeNtlmServer.cs b/src/libraries/Common/tests/System/Net/Security/FakeNtlmServer.cs index cb7a3a785e7faf..1117b3412f35a4 100644 --- a/src/libraries/Common/tests/System/Net/Security/FakeNtlmServer.cs +++ b/src/libraries/Common/tests/System/Net/Security/FakeNtlmServer.cs @@ -42,6 +42,8 @@ public FakeNtlmServer(NetworkCredential expectedCredential) public bool IsAuthenticated { get; private set; } public bool IsMICPresent { get; private set; } public string? ClientSpecifiedSpn { get; private set; } + public Flags InitialClientFlags { get; private set; } + public Flags NegotiatedFlags => _negotiatedFlags; private NetworkCredential _expectedCredential; @@ -83,7 +85,7 @@ private enum MessageType : uint } [Flags] - private enum Flags : uint + public enum Flags : uint { NegotiateUnicode = 0x00000001, NegotiateOEM = 0x00000002, @@ -177,17 +179,17 @@ private static ReadOnlySpan GetField(ReadOnlySpan payload, int field case MessageType.Negotiate: // We don't negotiate, we just verify Assert.True(incomingBlob.Length >= 32); - Flags flags = (Flags)BinaryPrimitives.ReadUInt32LittleEndian(incomingBlob.AsSpan(12, 4)); - Assert.Equal(_requiredFlags, (flags & _requiredFlags)); - Assert.True((flags & (Flags.NegotiateOEM | Flags.NegotiateUnicode)) != 0); - if (flags.HasFlag(Flags.NegotiateDomainSupplied)) + InitialClientFlags = (Flags)BinaryPrimitives.ReadUInt32LittleEndian(incomingBlob.AsSpan(12, 4)); + Assert.Equal(_requiredFlags, (InitialClientFlags & _requiredFlags)); + Assert.True((InitialClientFlags & (Flags.NegotiateOEM | Flags.NegotiateUnicode)) != 0); + if (InitialClientFlags.HasFlag(Flags.NegotiateDomainSupplied)) { string domain = Encoding.ASCII.GetString(GetField(incomingBlob, 16)); Assert.Equal(_expectedCredential.Domain, domain); } _expectedMessageType = MessageType.Authenticate; _negotiateMessage = incomingBlob; - return _challengeMessage = GenerateChallenge(flags); + return _challengeMessage = GenerateChallenge(InitialClientFlags); case MessageType.Authenticate: // Validate the authentication! diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs index 147dd35e4e194c..147097e521c700 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs @@ -270,8 +270,14 @@ public override void Dispose() { Debug.Assert(incomingBlob.IsEmpty); + Flags requiredFlags = s_requiredFlags; + if (_protectionLevel == ProtectionLevel.EncryptAndSign) + { + requiredFlags |= Flags.NegotiateSeal; + } + _negotiateMessage = new byte[sizeof(NegotiateMessage)]; - CreateNtlmNegotiateMessage(_negotiateMessage); + CreateNtlmNegotiateMessage(_negotiateMessage, requiredFlags); outgoingBlob = _negotiateMessage; statusCode = NegotiateAuthenticationStatusCode.ContinueNeeded; @@ -286,7 +292,7 @@ public override void Dispose() return outgoingBlob; } - private static unsafe void CreateNtlmNegotiateMessage(Span asBytes) + private static unsafe void CreateNtlmNegotiateMessage(Span asBytes, Flags requiredFlags) { Debug.Assert(HeaderLength == NtlmHeader.Length); Debug.Assert(asBytes.Length == sizeof(NegotiateMessage)); @@ -296,7 +302,7 @@ private static unsafe void CreateNtlmNegotiateMessage(Span asBytes) asBytes.Clear(); NtlmHeader.CopyTo(asBytes); message.Header.MessageType = MessageType.Negotiate; - message.Flags = s_requiredFlags; + message.Flags = requiredFlags; message.Version = s_version; } @@ -581,6 +587,13 @@ private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlyS return null; } + // We already negotiate signing, so we only need to check sealing/encryption. + if ((flags & Flags.NegotiateSeal) == 0 && _protectionLevel == ProtectionLevel.EncryptAndSign) + { + statusCode = NegotiateAuthenticationStatusCode.QopNotSupported; + return null; + } + ReadOnlySpan targetInfo = GetField(challengeMessage.TargetInfo, blob); byte[] targetInfoBuffer = ProcessTargetInfo(targetInfo, out DateTime time, out bool hasNbNames); @@ -615,7 +628,7 @@ private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlyS NtlmHeader.CopyTo(responseAsSpan); response.Header.MessageType = MessageType.Authenticate; - response.Flags = s_requiredFlags; + response.Flags = s_requiredFlags | (flags & Flags.NegotiateSeal); response.Version = s_version; // Calculate hash for hmac - same for lm2 and ntlm2 diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs index 033b82800dba94..48e3923037cc48 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs @@ -883,7 +883,16 @@ private async Task ReceiveBlobAsync(CancellationToken cancellationTo if (_framer.ReadHeader.MessageId == FrameHeader.HandshakeDoneId) { - _remoteOk = true; + if (HandshakeComplete && message.Length > 0) + { + Debug.Assert(_context != null); + _context.GetOutgoingBlob(message, out NegotiateAuthenticationStatusCode statusCode); + _remoteOk = statusCode is NegotiateAuthenticationStatusCode.Completed; + } + else + { + _remoteOk = true; + } } else if (_framer.ReadHeader.MessageId != FrameHeader.HandshakeId) { diff --git a/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs index 5be16753dc4926..4a4dbefa6f368c 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs @@ -211,6 +211,42 @@ public void NtlmIncorrectExchangeTest() Assert.False(fakeNtlmServer.IsAuthenticated); } + [ConditionalFact(nameof(IsNtlmAvailable))] + public void NtlmEncryptionTest() + { + using FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight); + + NegotiateAuthentication ntAuth = new NegotiateAuthentication( + new NegotiateAuthenticationClientOptions + { + Package = "NTLM", + Credential = s_testCredentialRight, + TargetName = "HTTP/foo", + RequiredProtectionLevel = ProtectionLevel.EncryptAndSign + }); + + NegotiateAuthenticationStatusCode statusCode; + byte[]? negotiateBlob = ntAuth.GetOutgoingBlob((byte[])null, out statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + Assert.NotNull(negotiateBlob); + + byte[]? challengeBlob = fakeNtlmServer.GetOutgoingBlob(negotiateBlob); + Assert.NotNull(challengeBlob); + // Validate that the client sent NegotiateSeal flag + Assert.Equal(FakeNtlmServer.Flags.NegotiateSeal, (fakeNtlmServer.InitialClientFlags & FakeNtlmServer.Flags.NegotiateSeal)); + + byte[]? authenticateBlob = ntAuth.GetOutgoingBlob(challengeBlob, out statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.Completed, statusCode); + Assert.NotNull(authenticateBlob); + + byte[]? empty = fakeNtlmServer.GetOutgoingBlob(authenticateBlob); + Assert.Null(empty); + Assert.True(fakeNtlmServer.IsAuthenticated); + + // Validate that the NegotiateSeal flag survived the full exchange + Assert.Equal(FakeNtlmServer.Flags.NegotiateSeal, (fakeNtlmServer.NegotiatedFlags & FakeNtlmServer.Flags.NegotiateSeal)); + } + [ConditionalFact(nameof(IsNtlmAvailable))] public void NtlmSignatureTest() {