Skip to content

Commit

Permalink
Fix NTAuthentication.MakeSignature/VerifySignature on Linux (#65679)
Browse files Browse the repository at this point in the history
* Add NTLM MakeSignature test, fix the output on Linux

* Add test for VerifySignature, make it working on macOS

* Use utf-8 string literals
  • Loading branch information
filipnavara committed May 12, 2022
1 parent 70b5d83 commit 7b5f40f
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 28 deletions.
15 changes: 5 additions & 10 deletions src/libraries/Common/src/System/Net/NTAuthentication.Managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,12 @@ internal sealed partial class NTAuthentication
// defined in winerror.h
private const int NTE_FAIL = unchecked((int)0x80090020);

private static ReadOnlySpan<byte> NtlmHeader => new byte[] {
(byte)'N', (byte)'T', (byte)'L', (byte)'M',
(byte)'S', (byte)'S', (byte)'P', 0 };
private static ReadOnlySpan<byte> 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<byte> ClientSigningKeyMagic => "session key to client-to-server signing key magic constant\0"u8;
private static ReadOnlySpan<byte> ServerSigningKeyMagic => "session key to server-to-client signing key magic constant\0"u8;
private static ReadOnlySpan<byte> ClientSealingKeyMagic => "session key to client-to-server sealing key magic constant\0"u8;
private static ReadOnlySpan<byte> ServerSealingKeyMagic => "session key to server-to-client sealing key magic constant\0"u8;

private static readonly byte[] s_workstation = Encoding.Unicode.GetBytes(Environment.MachineName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<byte>(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<byte>(buffer, offset, count));
return output.Length;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<byte> NtlmHeader => new byte[] {
(byte)'N', (byte)'T', (byte)'L', (byte)'M',
(byte)'S', (byte)'S', (byte)'P', 0 };
private static ReadOnlySpan<byte> NtlmHeader => "NTLMSSP\0"u8;
private static ReadOnlySpan<byte> ClientSigningKeyMagic => "session key to client-to-server signing key magic constant\0"u8;
private static ReadOnlySpan<byte> ServerSigningKeyMagic => "session key to server-to-client signing key magic constant\0"u8;
private static ReadOnlySpan<byte> ClientSealingKeyMagic => "session key to client-to-server sealing key magic constant\0"u8;
private static ReadOnlySpan<byte> ServerSealingKeyMagic => "session key to server-to-client sealing key magic constant\0"u8;

private enum MessageType : uint
{
Expand Down Expand Up @@ -257,6 +265,17 @@ private byte[] MakeNtlm2Hash()
}
}

// Section 3.4.5.2 SIGNKEY, 3.4.5.3 SEALKEY
private byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlySpan<byte> magic)
{
using (var md5 = IncrementalHash.CreateHash(HashAlgorithmName.MD5))
{
md5.AppendData(exportedSessionKey);
md5.AppendData(magic);
return md5.GetHashAndReset();
}
}

private void ValidateAuthentication(byte[] incomingBlob)
{
ReadOnlySpan<byte> lmChallengeResponse = GetField(incomingBlob, 12);
Expand Down Expand Up @@ -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<byte> message,
uint sequenceNumber,
ReadOnlySpan<byte> signingKey,
RC4 seal,
Span<byte> 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<byte> hmacResult = stackalloc byte[hmac.HashLengthInBytes];
hmac.GetHashAndReset(hmacResult);
seal.Transform(hmacResult.Slice(0, 8), signature.Slice(4, 8));
}
}

public void VerifyMIC(ReadOnlySpan<byte> message, ReadOnlySpan<byte> 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<byte> expectedSignature = stackalloc byte[16];
CalculateSignature(message, sequenceNumber, _clientSigningKey, _clientSeal, expectedSignature);
Assert.True(signature.SequenceEqual(expectedSignature));
}

public void GetMIC(ReadOnlySpan<byte> message, Span<byte> signature, uint sequenceNumber)
{
// Make sure the authentication finished
Assert.NotNull(_serverSeal);
Assert.NotNull(_serverSigningKey);

CalculateSignature(message, sequenceNumber, _serverSigningKey, _serverSeal, signature);
}

public void Unseal(ReadOnlySpan<byte> sealedMessage, Span<byte> message)
{
_clientSeal.Transform(sealedMessage, message);
}

public void Seal(ReadOnlySpan<byte> message, Span<byte> sealedMessage)
{
_serverSeal.Transform(message, sealedMessage);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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()
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 7b5f40f

Please sign in to comment.