Skip to content

Commit

Permalink
Add DeriveKeyFromSessionKey API for auth
Browse files Browse the repository at this point in the history
Added the DeriveKeyFromSessionKey function to NegotiateAuthentication
which can be used to retrieve the session key negotiated between the
client and server in a negotiated authentication context. This can be
used by protocols that do not use the builtin wrapping mechanisms but
instead derive their own authentication method from the session key.

Fix #111099
  • Loading branch information
jborean93 committed Feb 27, 2025
1 parent ead9efe commit 619c7c8
Show file tree
Hide file tree
Showing 20 changed files with 293 additions and 10 deletions.
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);
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

0 comments on commit 619c7c8

Please sign in to comment.