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() {