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

Add DeriveKeyFromSessionKey API for auth #113003

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -285,5 +285,11 @@ internal static unsafe Status VerifyMic(
return VerifyMic(out minorStatus, contextHandle, inputBytesPtr, inputBytes.Length, tokenBytesPtr, tokenBytes.Length);
}
}

[LibraryImport(Interop.Libraries.NetSecurityNative, EntryPoint = "NetSecurityNative_InquireSecContextSessionKey")]
internal static partial Status InquireSecContextSessionKey(
out Status minorStatus,
SafeGssContextHandle? contextHandle,
ref GssBuffer outBuffer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ internal enum ContextAttribute
SECPKG_ATTR_DCE_INFO = 3,
SECPKG_ATTR_STREAM_SIZES = 4,
SECPKG_ATTR_AUTHORITY = 6,
SECPKG_ATTR_SESSION_KEY = 9,
SECPKG_ATTR_PACKAGE_INFO = 10,
SECPKG_ATTR_NEGOTIATION_INFO = 12,
SECPKG_ATTR_UNIQUE_BINDINGS = 25,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

namespace System.Net
{
// sspi.h
[StructLayout(LayoutKind.Sequential)]
internal struct SecPkgContext_SessionKey
{
public int SessionKeyLength;
public nint SessionKey;
}
}
4 changes: 4 additions & 0 deletions src/libraries/System.Net.Security/ref/System.Net.Security.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public void Dispose() { }
public System.Net.Security.NegotiateAuthenticationStatusCode Wrap(System.ReadOnlySpan<byte> input, System.Buffers.IBufferWriter<byte> outputWriter, bool requestEncryption, out bool isEncrypted) { throw null; }
public void ComputeIntegrityCheck(System.ReadOnlySpan<byte> message, System.Buffers.IBufferWriter<byte> signatureWriter) { }
public bool VerifyIntegrityCheck(System.ReadOnlySpan<byte> message, System.ReadOnlySpan<byte> signature) { throw null; }
public void DeriveKeyFromSessionKey(Action<ReadOnlySpan<byte>> keyDerivationFunction) { }
public void DeriveKeyFromSessionKey<TState>(Action<ReadOnlySpan<byte>, TState> keyDerivationFunction, TState state) { }
public TReturn DeriveKeyFromSessionKey<TReturn>(Func<ReadOnlySpan<byte>, TReturn> keyDerivationFunction) { throw null; }
public TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state) { throw null; }
}
public partial class NegotiateAuthenticationClientOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@
Link="Common\Interop\Windows\SspiCli\SecPkgContext_NegotiationInfoW.cs" />
<Compile Include="$(CommonPath)Interop\Windows\SspiCli\NegotiationInfoClass.cs"
Link="Common\Interop\Windows\SspiCli\NegotiationInfoClass.cs" />
<Compile Include="$(CommonPath)Interop\Windows\SspiCli\SecPkgContext_SessionKey.cs"
Link="Common\Interop\Windows\SspiCli\SecPkgContext_SessionKey.cs" />
<Compile Include="$(CommonPath)Interop\Windows\SspiCli\SecPkgContext_Sizes.cs"
Link="Common\Interop\Windows\SspiCli\SecPkgContext_Sizes.cs" />
<Compile Include="$(CommonPath)System\Collections\Generic\BidirectionalDictionary.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal sealed class ManagedNtlmNegotiateAuthenticationPal : NegotiateAuthentic
private readonly ProtectionLevel _protectionLevel;

// State parameters
private byte[]? _exportedSessionKey;
private byte[]? _negotiateMessage;
private byte[]? _clientSigningKey;
private byte[]? _serverSigningKey;
Expand Down Expand Up @@ -247,6 +248,11 @@ private ManagedNtlmNegotiateAuthenticationPal(NegotiateAuthenticationClientOptio
public override void Dispose()
{
// Dispose of the state
if (_exportedSessionKey is not null)
{
CryptographicOperations.ZeroMemory(_exportedSessionKey);
}
_exportedSessionKey = null;
_negotiateMessage = null;
_clientSigningKey = null;
_serverSigningKey = null;
Expand Down Expand Up @@ -654,8 +660,8 @@ private static byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlyS
AddToPayload(ref response.Workstation, s_workstation, payload, ref payloadOffset);

// Generate random session key that will be used for signing the messages
Span<byte> exportedSessionKey = stackalloc byte[16];
RandomNumberGenerator.Fill(exportedSessionKey);
_exportedSessionKey = new byte[SessionKeyLength];
RandomNumberGenerator.Fill(_exportedSessionKey);

// Both flags are necessary to exchange keys needed for MIC (!)
Debug.Assert(flags.HasFlag(Flags.NegotiateSign) && flags.HasFlag(Flags.NegotiateKeyExchange));
Expand All @@ -669,14 +675,14 @@ private static byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlyS
using (RC4 rc4 = new RC4(sessionBaseKey))
{
Span<byte> encryptedRandomSessionKey = payload.Slice(payloadOffset, 16);
rc4.Transform(exportedSessionKey, encryptedRandomSessionKey);
rc4.Transform(_exportedSessionKey, encryptedRandomSessionKey);
SetField(ref response.EncryptedRandomSessionKey, 16, payloadOffset);
payloadOffset += 16;
}

// Calculate MIC
Debug.Assert(_negotiateMessage != null);
using (var hmacMic = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, exportedSessionKey))
using (var hmacMic = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, _exportedSessionKey))
{
hmacMic.AppendData(_negotiateMessage);
hmacMic.AppendData(blob);
Expand All @@ -685,14 +691,13 @@ private static byte[] DeriveKey(ReadOnlySpan<byte> exportedSessionKey, ReadOnlyS
}

// Derive signing keys
_clientSigningKey = DeriveKey(exportedSessionKey, ClientSigningKeyMagic);
_serverSigningKey = DeriveKey(exportedSessionKey, ServerSigningKeyMagic);
_clientSealingKey = DeriveKey(exportedSessionKey, ClientSealingKeyMagic);
_serverSealingKey = DeriveKey(exportedSessionKey, ServerSealingKeyMagic);
_clientSigningKey = DeriveKey(_exportedSessionKey, ClientSigningKeyMagic);
_serverSigningKey = DeriveKey(_exportedSessionKey, ServerSigningKeyMagic);
_clientSealingKey = DeriveKey(_exportedSessionKey, ClientSealingKeyMagic);
_serverSealingKey = DeriveKey(_exportedSessionKey, ServerSealingKeyMagic);
ResetKeys();
_clientSequenceNumber = 0;
_serverSequenceNumber = 0;
CryptographicOperations.ZeroMemory(exportedSessionKey);

Debug.Assert(payloadOffset == responseBytes.Length);

Expand Down Expand Up @@ -829,6 +834,16 @@ public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span<byte> input

return NegotiateAuthenticationStatusCode.Completed;
}

public override TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state)
{
if (_exportedSessionKey is null)
{
throw new InvalidOperationException(SR.net_auth_noauth);
}

return keyDerivationFunction(_exportedSessionKey, state);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,16 @@ public override void GetMIC(ReadOnlySpan<byte> message, IBufferWriter<byte> sign

_mechanism.GetMIC(message, signature);
}

public override TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state)
{
if (_mechanism is null || !_isAuthenticated)
{
throw new InvalidOperationException(SR.net_auth_noauth);
}

return _mechanism.DeriveKeyFromSessionKey(keyDerivationFunction, state);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,31 @@ public override unsafe bool VerifyMIC(ReadOnlySpan<byte> message, ReadOnlySpan<b
return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE;
}

public override TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state)
{
Debug.Assert(_securityContext is not null);

Interop.NetSecurityNative.GssBuffer keyBuffer = default;
try
{
Interop.NetSecurityNative.Status minorStatus;
Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.InquireSecContextSessionKey(
out minorStatus,
_securityContext,
ref keyBuffer);
if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE)
{
throw new Interop.NetSecurityNative.GssApiException(status, minorStatus);
}

return keyDerivationFunction(keyBuffer.Span, state);
}
finally
{
keyBuffer.Dispose();
}
}

private static Interop.NetSecurityNative.PackageType GetPackageType(string package)
{
if (string.Equals(package, NegotiationInfoClass.Negotiate, StringComparison.OrdinalIgnoreCase))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public override void Dispose()
public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span<byte> input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) => throw new InvalidOperationException();
public override void GetMIC(ReadOnlySpan<byte> message, IBufferWriter<byte> signature) => throw new InvalidOperationException();
public override bool VerifyMIC(ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature) => throw new InvalidOperationException();
public override TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state) => throw new InvalidOperationException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,29 @@ public override unsafe bool VerifyMIC(ReadOnlySpan<byte> message, ReadOnlySpan<b
}
}

public override unsafe TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state)
{
Debug.Assert(_securityContext is not null);

SecPkgContext_SessionKey sessionKey = default;
int result = Interop.SspiCli.QueryContextAttributesW(ref _securityContext._handle, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SESSION_KEY, &sessionKey);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some other APIs use SSPIWrapper.QueryBlittableContextAttributes but that doesn't seem to great with this API as:

  • It returns a bool to see if it worked or not which makes it hard to get the original error to the caller on a failure
  • The whole SafeHandle and freeing setup seems quite complicated
    • This is compounded because it seems like I need a custom handle to point to the SessionKey field

So in the end I just manually call the API and FreeContextBuffer the required address. Happy to change it if needed but I wasn't fully sure what the benefits really would be

if (result != 0)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, SR.Format(SR.net_log_operation_failed_with_error, nameof(Interop.SspiCli.QueryContextAttributesW), $"0x{result:X}"));
throw new Win32Exception(result);
}

try
{
ReadOnlySpan<byte> key = new ReadOnlySpan<byte>((void*)sessionKey.SessionKey, sessionKey.SessionKeyLength);
return keyDerivationFunction(key, state);
}
finally
{
Interop.SspiCli.FreeContextBuffer(sessionKey.SessionKey);
}
}

private static SafeFreeCredentials AcquireDefaultCredential(string package, bool isServer)
{
return SSPIWrapper.AcquireDefaultCredential(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ internal abstract partial class NegotiateAuthenticationPal : IDisposable
public abstract NegotiateAuthenticationStatusCode UnwrapInPlace(Span<byte> input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted);
public abstract void GetMIC(ReadOnlySpan<byte> message, IBufferWriter<byte> signature);
public abstract bool VerifyMIC(ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature);
public abstract TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,58 @@ public bool VerifyIntegrityCheck(ReadOnlySpan<byte> message, ReadOnlySpan<byte>
return _pal.VerifyMIC(message, signature);
}

/// <summary>
/// Derive a key from the negotiate authentication's session key.
/// </summary>
/// <param name="keyDerivationFunction">A callback that receives the session key.</param>
/// <exception cref="InvalidOperationException">Authentication failed or has not occurred.</exception>
public void DeriveKeyFromSessionKey(Action<ReadOnlySpan<byte>> keyDerivationFunction) =>
DeriveKeyFromSessionKey(static (key, state) => state(key), keyDerivationFunction);

/// <summary>
/// Derive a key from the negotiate authentication's session key with the provided state.
/// </summary>
/// <param name="keyDerivationFunction">A callback that receives the session key.</param>
/// <param name="state">The element to pass to <paramref name="keyDerivationFunction"/>.</param>
/// <exception cref="InvalidOperationException">Authentication failed or has not occurred.</exception>
public void DeriveKeyFromSessionKey<TState>(Action<ReadOnlySpan<byte>, TState> keyDerivationFunction, TState state) =>
DeriveKeyFromSessionKey(
static (key, state) =>
{
state.Kdf(key, state.State);
return 0;
},
(Kdf: keyDerivationFunction, State: state));

/// <summary>
/// Derive a key from the negotiate authentication's session key.
/// </summary>
/// <typeparam name="TReturn">The return type of <paramref name="keyDerivationFunction"/>.</typeparam>
/// <param name="keyDerivationFunction">A callback that receives the session key and returns a value.</param>
/// <returns>The value returned by <paramref name="keyDerivationFunction"/>.</returns>
/// <exception cref="InvalidOperationException">Authentication failed or has not occurred.</exception>
public TReturn DeriveKeyFromSessionKey<TReturn>(Func<ReadOnlySpan<byte>, TReturn> keyDerivationFunction) =>
DeriveKeyFromSessionKey(static (key, state) => state(key), keyDerivationFunction);

/// <summary>
/// Derive a key from the negotiate authentication's session key with the provided state.
/// </summary>
/// <typeparam name="TState">The type of the state element.</typeparam>
/// <typeparam name="TReturn">The return type of <paramref name="keyDerivationFunction"/>.</typeparam>
/// <param name="keyDerivationFunction">A callback that receives the session key and returns a value.</param>
/// <param name="state">The element to pass to <paramref name="keyDerivationFunction"/>.</param>
/// <returns>The value returned by <paramref name="keyDerivationFunction"/>.</returns>
/// <exception cref="InvalidOperationException">Authentication failed or has not occurred.</exception>
public TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state)
{
if (!IsAuthenticated || _isDisposed)
{
throw new InvalidOperationException(SR.net_auth_noauth);
}

return _pal.DeriveKeyFromSessionKey(keyDerivationFunction, state);
}

private bool CheckSpn()
{
Debug.Assert(_extendedProtectionPolicy != null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public NegotiateAuthenticationKerberosTest(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}

[Fact]
public async Task Loopback_Success()
{
Expand Down Expand Up @@ -58,6 +58,10 @@ await kerberosExecutor.Invoke(() =>
Assert.Equal("Kerberos", serverNegotiateAuthentication.Package);
Assert.True(clientNegotiateAuthentication.IsAuthenticated);
Assert.True(serverNegotiateAuthentication.IsAuthenticated);

byte[] clientKey = clientNegotiateAuthentication.DeriveKeyFromSessionKey(static (k) => k.ToArray());
byte[] serverKey = serverNegotiateAuthentication.DeriveKeyFromSessionKey(static (k) => k.ToArray());
Assert.Equal(clientKey, serverKey);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,53 @@ public void RemoteIdentity_ThrowsOnDisposed()
}
}

[Fact]
public void DeriveKeyFromSessionKey_ThrowsOnUnauthenticated()
{
NegotiateAuthenticationClientOptions clientOptions = new NegotiateAuthenticationClientOptions { Credential = s_testCredentialRight, TargetName = "HTTP/foo" };
NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication(clientOptions);
Assert.Throws<InvalidOperationException>(() => negotiateAuthentication.DeriveKeyFromSessionKey(static (k) => throw new Exception()));
}

[ConditionalFact(nameof(IsNtlmAvailable))]
public void DeriveKeyFromSessionKey()
{
using FakeNtlmServer fakeNtlmServer = new FakeNtlmServer(s_testCredentialRight);
NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication(
new NegotiateAuthenticationClientOptions
{
Package = "NTLM",
Credential = s_testCredentialRight,
TargetName = "HTTP/foo",
RequiredProtectionLevel = ProtectionLevel.Sign
});

DoNtlmExchange(fakeNtlmServer, negotiateAuthentication);

Assert.True(fakeNtlmServer.IsAuthenticated);
Assert.True(negotiateAuthentication.IsAuthenticated);

byte[] sessionKey = negotiateAuthentication.DeriveKeyFromSessionKey(static (k) => k.ToArray());
Assert.Equal(16, sessionKey.Length); // NTLM is always 16

negotiateAuthentication.DeriveKeyFromSessionKey((k) =>
{
Assert.Equal(sessionKey, k);
});

negotiateAuthentication.DeriveKeyFromSessionKey(static (k, s) =>
{
Assert.Equal(s, k);
}, sessionKey);

bool res = negotiateAuthentication.DeriveKeyFromSessionKey(static (k, s) =>
{
Assert.Equal(s, k);
return true;
}, sessionKey);
Assert.True(res);
}

[Fact]
public void Package_Unsupported()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@
Link="Common\Interop\Windows\SspiCli\SecurityPackageInfoClass.cs" />
<Compile Include="$(CommonPath)Interop\Windows\SspiCli\SecurityPackageInfo.cs"
Link="Common\Interop\Windows\SspiCli\SecurityPackageInfo.cs" />
<Compile Include="$(CommonPath)Interop\Windows\SspiCli\SecPkgContext_SessionKey.cs"
Link="Common\Interop\Windows\SspiCli\SecPkgContext_SessionKey.cs" />
<Compile Include="$(CommonPath)Interop\Windows\SspiCli\SecPkgContext_Sizes.cs"
Link="Common\Interop\Windows\SspiCli\SecPkgContext_Sizes.cs" />
<Compile Include="$(CommonPath)Interop\Windows\SspiCli\SafeDeleteContext.cs"
Expand Down
1 change: 1 addition & 0 deletions src/native/libs/Common/pal_config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
#cmakedefine01 HAVE_TCP_H_TCPSTATE_ENUM
#cmakedefine01 HAVE_TCP_FSM_H
#cmakedefine01 HAVE_GSSFW_HEADERS
#cmakedefine01 HAVE_GSS_C_INQ_SSPI_SESSION_KEY
#cmakedefine01 HAVE_GSS_SPNEGO_MECHANISM
#cmakedefine01 HAVE_HEIMDAL_HEADERS
#cmakedefine01 HAVE_NSGETENVIRON
Expand Down
Loading
Loading