diff --git a/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs b/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs index 4593a767ca791..335550d9f9c4f 100644 --- a/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs +++ b/src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs @@ -43,17 +43,12 @@ internal sealed partial class NTAuthentication // defined in winerror.h private const int NTE_FAIL = unchecked((int)0x80090020); - private static ReadOnlySpan NtlmHeader => new byte[] { - (byte)'N', (byte)'T', (byte)'L', (byte)'M', - (byte)'S', (byte)'S', (byte)'P', 0 }; + private static ReadOnlySpan NtlmHeader => "NTLMSSP\0"u8; - private static byte[] ClientSigningKeyMagic = Encoding.ASCII.GetBytes("session key to client-to-server signing key magic constant\0"); - - private static byte[] ServerSigningKeyMagic = Encoding.ASCII.GetBytes("session key to server-to-client signing key magic constant\0"); - - private static byte[] ClientSealingKeyMagic = Encoding.ASCII.GetBytes("session key to client-to-server sealing key magic constant\0"); - - private static byte[] ServerSealingKeyMagic = Encoding.ASCII.GetBytes("session key to server-to-client sealing key magic constant\0"); + private static ReadOnlySpan ClientSigningKeyMagic => "session key to client-to-server signing key magic constant\0"u8; + private static ReadOnlySpan ServerSigningKeyMagic => "session key to server-to-client signing key magic constant\0"u8; + private static ReadOnlySpan ClientSealingKeyMagic => "session key to client-to-server sealing key magic constant\0"u8; + private static ReadOnlySpan ServerSealingKeyMagic => "session key to server-to-client sealing key magic constant\0"u8; private static readonly byte[] s_workstation = Encoding.Unicode.GetBytes(Environment.MachineName); diff --git a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs index 9a867bc939689..448444543018e 100644 --- a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Buffers; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -618,21 +619,8 @@ internal static int VerifySignature(SafeDeleteContext securityContext, byte[] bu internal static int MakeSignature(SafeDeleteContext securityContext, byte[] buffer, int offset, int count, [AllowNull] ref byte[] output) { SafeDeleteNegoContext gssContext = (SafeDeleteNegoContext)securityContext; - byte[] tempOutput = GssWrap(gssContext.GssContext, false, new ReadOnlySpan(buffer, offset, count)); - // Create space for prefixing with the length - const int prefixLength = 4; - output = new byte[tempOutput.Length + prefixLength]; - Array.Copy(tempOutput, 0, output, prefixLength, tempOutput.Length); - int resultSize = tempOutput.Length; - unchecked - { - output[0] = (byte)((resultSize) & 0xFF); - output[1] = (byte)(((resultSize) >> 8) & 0xFF); - output[2] = (byte)(((resultSize) >> 16) & 0xFF); - output[3] = (byte)(((resultSize) >> 24) & 0xFF); - } - - return resultSize + 4; + output = GssWrap(gssContext.GssContext, false, new ReadOnlySpan(buffer, offset, count)); + return output.Length; } } } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeNtlmServer.cs b/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeNtlmServer.cs index ffb78a5c88f24..780ff0fbe10ca 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeNtlmServer.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeNtlmServer.cs @@ -48,6 +48,12 @@ public FakeNtlmServer(NetworkCredential expectedCredential) private byte[]? _negotiateMessage; private byte[]? _challengeMessage; + // Established signing and sealing keys + private byte[]? _clientSigningKey; + private byte[]? _serverSigningKey; + internal RC4? _clientSeal; + internal RC4? _serverSeal; + private MessageType _expectedMessageType = MessageType.Negotiate; // Minimal set of required negotiation flags @@ -57,9 +63,11 @@ public FakeNtlmServer(NetworkCredential expectedCredential) // Fixed server challenge (same value as in Protocol Examples section of the specification) private byte[] _serverChallenge = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }; - private static ReadOnlySpan NtlmHeader => new byte[] { - (byte)'N', (byte)'T', (byte)'L', (byte)'M', - (byte)'S', (byte)'S', (byte)'P', 0 }; + private static ReadOnlySpan NtlmHeader => "NTLMSSP\0"u8; + private static ReadOnlySpan ClientSigningKeyMagic => "session key to client-to-server signing key magic constant\0"u8; + private static ReadOnlySpan ServerSigningKeyMagic => "session key to server-to-client signing key magic constant\0"u8; + private static ReadOnlySpan ClientSealingKeyMagic => "session key to client-to-server sealing key magic constant\0"u8; + private static ReadOnlySpan ServerSealingKeyMagic => "session key to server-to-client sealing key magic constant\0"u8; private enum MessageType : uint { @@ -257,6 +265,17 @@ private byte[] MakeNtlm2Hash() } } + // Section 3.4.5.2 SIGNKEY, 3.4.5.3 SEALKEY + private byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlySpan magic) + { + using (var md5 = IncrementalHash.CreateHash(HashAlgorithmName.MD5)) + { + md5.AppendData(exportedSessionKey); + md5.AppendData(magic); + return md5.GetHashAndReset(); + } + } + private void ValidateAuthentication(byte[] incomingBlob) { ReadOnlySpan lmChallengeResponse = GetField(incomingBlob, 12); @@ -354,6 +373,65 @@ private void ValidateAuthentication(byte[] incomingBlob) } Assert.Equal(mic.ToArray(), calculatedMic); } + + // Derive signing keys + _clientSigningKey = DeriveKey(exportedSessionKey, ClientSigningKeyMagic); + _serverSigningKey = DeriveKey(exportedSessionKey, ServerSigningKeyMagic); + _clientSeal = new RC4(DeriveKey(exportedSessionKey, ClientSealingKeyMagic)); + _serverSeal = new RC4(DeriveKey(exportedSessionKey, ServerSealingKeyMagic)); + CryptographicOperations.ZeroMemory(exportedSessionKey); + } + + private void CalculateSignature( + ReadOnlySpan message, + uint sequenceNumber, + ReadOnlySpan signingKey, + RC4 seal, + Span signature) + { + BinaryPrimitives.WriteInt32LittleEndian(signature, 1); + BinaryPrimitives.WriteUInt32LittleEndian(signature.Slice(12), sequenceNumber); + using (var hmac = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, signingKey)) + { + hmac.AppendData(signature.Slice(12, 4)); + hmac.AppendData(message); + Span hmacResult = stackalloc byte[hmac.HashLengthInBytes]; + hmac.GetHashAndReset(hmacResult); + seal.Transform(hmacResult.Slice(0, 8), signature.Slice(4, 8)); + } + } + + public void VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature, uint sequenceNumber) + { + Assert.Equal(16, signature.Length); + // Check version + Assert.Equal(1, BinaryPrimitives.ReadInt32LittleEndian(signature)); + // Make sure the authentication finished + Assert.NotNull(_clientSeal); + Assert.NotNull(_clientSigningKey); + + Span expectedSignature = stackalloc byte[16]; + CalculateSignature(message, sequenceNumber, _clientSigningKey, _clientSeal, expectedSignature); + Assert.True(signature.SequenceEqual(expectedSignature)); + } + + public void GetMIC(ReadOnlySpan message, Span signature, uint sequenceNumber) + { + // Make sure the authentication finished + Assert.NotNull(_serverSeal); + Assert.NotNull(_serverSigningKey); + + CalculateSignature(message, sequenceNumber, _serverSigningKey, _serverSeal, signature); + } + + public void Unseal(ReadOnlySpan sealedMessage, Span message) + { + _clientSeal.Transform(sealedMessage, message); + } + + public void Seal(ReadOnlySpan message, Span sealedMessage) + { + _serverSeal.Transform(message, sealedMessage); } } } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs b/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs index 6a90b7665f944..4f3298414b88e 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/NTAuthenticationTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Buffers.Binary; using System.IO; using System.Net.Security; using System.Text; @@ -16,6 +18,7 @@ public class NTAuthenticationTests private static NetworkCredential s_testCredentialRight = new NetworkCredential("rightusername", "rightpassword"); private static NetworkCredential s_testCredentialWrong = new NetworkCredential("rightusername", "wrongpassword"); + private static byte[] s_Hello => "Hello"u8; [Fact] public void NtlmProtocolExampleTest() @@ -104,6 +107,41 @@ public void NtlmIncorrectExchangeTest() Assert.False(fakeNtlmServer.IsAuthenticated); } + [ConditionalFact(nameof(IsNtlmInstalled))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/65678", TestPlatforms.OSX)] + public void NtlmSignatureTest() + { + FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight); + NTAuthentication ntAuth = new NTAuthentication( + isServer: false, "NTLM", s_testCredentialRight, "HTTP/foo", + ContextFlagsPal.Connection | ContextFlagsPal.InitIntegrity | ContextFlagsPal.Confidentiality, null); + + DoNtlmExchange(fakeNtlmServer, ntAuth); + + Assert.True(fakeNtlmServer.IsAuthenticated); + + // Test MakeSignature on client side and decoding it on server side + byte[]? output = null; + int len = ntAuth.MakeSignature(s_Hello, 0, s_Hello.Length, ref output); + Assert.NotNull(output); + Assert.Equal(16 + s_Hello.Length, len); + // Unseal the content and check it + byte[] temp = new byte[s_Hello.Length]; + fakeNtlmServer.Unseal(output.AsSpan(16), temp); + Assert.Equal(s_Hello, temp); + // Check the signature + fakeNtlmServer.VerifyMIC(temp, output.AsSpan(0, 16), sequenceNumber: 0); + + // Test creating signature on server side and decoding it with VerifySignature on client side + byte[] serverSignedMessage = new byte[16 + s_Hello.Length]; + fakeNtlmServer.Seal(s_Hello, serverSignedMessage.AsSpan(16, s_Hello.Length)); + fakeNtlmServer.GetMIC(s_Hello, serverSignedMessage.AsSpan(0, 16), sequenceNumber: 0); + len = ntAuth.VerifySignature(serverSignedMessage, 0, serverSignedMessage.Length); + Assert.Equal(s_Hello.Length, len); + // NOTE: VerifySignature doesn't return the content on Windows + // Assert.Equal(s_Hello, serverSignedMessage.AsSpan(0, len).ToArray()); + } + private void DoNtlmExchange(FakeNtlmServer fakeNtlmServer, NTAuthentication ntAuth) { byte[]? negotiateBlob = ntAuth.GetOutgoingBlob(null, throwOnError: false);