Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix NTAuthentication.MakeSignature/VerifySignature on Linux #65679

Merged
merged 3 commits into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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