diff --git a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs index e87970e2a9dd8e..cfbfebb27db2c6 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs @@ -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); } } diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs index cbff2b2798efed..0cb7252a5bf155 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -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, diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SecPkgContext_SessionKey.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SecPkgContext_SessionKey.cs new file mode 100644 index 00000000000000..e3ed2e44d309b2 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SecPkgContext_SessionKey.cs @@ -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; + } +} diff --git a/src/libraries/System.Net.Security/ref/System.Net.Security.cs b/src/libraries/System.Net.Security/ref/System.Net.Security.cs index 986a492e479212..1bb5411245ceeb 100644 --- a/src/libraries/System.Net.Security/ref/System.Net.Security.cs +++ b/src/libraries/System.Net.Security/ref/System.Net.Security.cs @@ -59,6 +59,10 @@ public void Dispose() { } public System.Net.Security.NegotiateAuthenticationStatusCode Wrap(System.ReadOnlySpan input, System.Buffers.IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted) { throw null; } public void ComputeIntegrityCheck(System.ReadOnlySpan message, System.Buffers.IBufferWriter signatureWriter) { } public bool VerifyIntegrityCheck(System.ReadOnlySpan message, System.ReadOnlySpan signature) { throw null; } + public void DeriveKeyFromSessionKey(Action> keyDerivationFunction) { } + public void DeriveKeyFromSessionKey(Action, TState> keyDerivationFunction, TState state) { } + public TReturn DeriveKeyFromSessionKey(Func, TReturn> keyDerivationFunction) { throw null; } + public TReturn DeriveKeyFromSessionKey(Func, TState, TReturn> keyDerivationFunction, TState state) { throw null; } } public partial class NegotiateAuthenticationClientOptions { diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index ecf71f7c0f5c97..4649d91a2bb890 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -249,6 +249,8 @@ Link="Common\Interop\Windows\SspiCli\SecPkgContext_NegotiationInfoW.cs" /> + exportedSessionKey, ReadOnlyS AddToPayload(ref response.Workstation, s_workstation, payload, ref payloadOffset); // Generate random session key that will be used for signing the messages - Span 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)); @@ -669,14 +675,14 @@ private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlyS using (RC4 rc4 = new RC4(sessionBaseKey)) { Span 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); @@ -685,14 +691,13 @@ private static byte[] DeriveKey(ReadOnlySpan 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); @@ -829,6 +834,16 @@ public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span input return NegotiateAuthenticationStatusCode.Completed; } + + public override TReturn DeriveKeyFromSessionKey(Func, TState, TReturn> keyDerivationFunction, TState state) + { + if (_exportedSessionKey is null) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + return keyDerivationFunction(_exportedSessionKey, state); + } } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs index 1ddb004769037f..654d8d48e2c3e9 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs @@ -450,6 +450,16 @@ public override void GetMIC(ReadOnlySpan message, IBufferWriter sign _mechanism.GetMIC(message, signature); } + + public override TReturn DeriveKeyFromSessionKey(Func, TState, TReturn> keyDerivationFunction, TState state) + { + if (_mechanism is null || !_isAuthenticated) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + return _mechanism.DeriveKeyFromSessionKey(keyDerivationFunction, state); + } } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs index 77c3a3454b1ce6..a9c4d571d08c71 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs @@ -457,6 +457,31 @@ public override unsafe bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan(Func, 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)) diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs index 1cdeef37fad544..0094d5c67cffdd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs @@ -54,6 +54,7 @@ public override void Dispose() public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) => throw new InvalidOperationException(); public override void GetMIC(ReadOnlySpan message, IBufferWriter signature) => throw new InvalidOperationException(); public override bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) => throw new InvalidOperationException(); + public override TReturn DeriveKeyFromSessionKey(Func, TState, TReturn> keyDerivationFunction, TState state) => throw new InvalidOperationException(); } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Windows.cs index 8e86d3dc91ca7e..8b382661acc928 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Windows.cs @@ -664,6 +664,29 @@ public override unsafe bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan(Func, 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 key = new ReadOnlySpan((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( diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.cs index 4f6e44ee3e375a..1458235c568857 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.cs @@ -25,5 +25,6 @@ internal abstract partial class NegotiateAuthenticationPal : IDisposable public abstract NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted); public abstract void GetMIC(ReadOnlySpan message, IBufferWriter signature); public abstract bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature); + public abstract TReturn DeriveKeyFromSessionKey(Func, TState, TReturn> keyDerivationFunction, TState state); } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs index 0042f73605919f..b013213bb64aee 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs @@ -401,6 +401,58 @@ public bool VerifyIntegrityCheck(ReadOnlySpan message, ReadOnlySpan return _pal.VerifyMIC(message, signature); } + /// + /// Derive a key from the negotiate authentication's session key. + /// + /// A callback that receives the session key. + /// Authentication failed or has not occurred. + public void DeriveKeyFromSessionKey(Action> keyDerivationFunction) => + DeriveKeyFromSessionKey(static (key, state) => state(key), keyDerivationFunction); + + /// + /// Derive a key from the negotiate authentication's session key with the provided state. + /// + /// A callback that receives the session key. + /// The element to pass to . + /// Authentication failed or has not occurred. + public void DeriveKeyFromSessionKey(Action, TState> keyDerivationFunction, TState state) => + DeriveKeyFromSessionKey( + static (key, state) => + { + state.Kdf(key, state.State); + return 0; + }, + (Kdf: keyDerivationFunction, State: state)); + + /// + /// Derive a key from the negotiate authentication's session key. + /// + /// The return type of . + /// A callback that receives the session key and returns a value. + /// The value returned by . + /// Authentication failed or has not occurred. + public TReturn DeriveKeyFromSessionKey(Func, TReturn> keyDerivationFunction) => + DeriveKeyFromSessionKey(static (key, state) => state(key), keyDerivationFunction); + + /// + /// Derive a key from the negotiate authentication's session key with the provided state. + /// + /// The type of the state element. + /// The return type of . + /// A callback that receives the session key and returns a value. + /// The element to pass to . + /// The value returned by . + /// Authentication failed or has not occurred. + public TReturn DeriveKeyFromSessionKey(Func, 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); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateAuthenticationKerberosTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateAuthenticationKerberosTest.cs index d7149f8daf62d7..d6e2cdba87c698 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateAuthenticationKerberosTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateAuthenticationKerberosTest.cs @@ -17,7 +17,7 @@ public NegotiateAuthenticationKerberosTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } - + [Fact] public async Task Loopback_Success() { @@ -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); }); } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs index 2120d49a924f30..a23ba660b354dc 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs @@ -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(() => 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() { diff --git a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj index d35de6d843f3d6..89b571858ea51b 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj @@ -289,6 +289,8 @@ Link="Common\Interop\Windows\SspiCli\SecurityPackageInfoClass.cs" /> + count < 1) + { + gss_release_buffer_set(&minorStatusFree, &sessionKey); + return GSS_S_FAILURE; + } + + // It is difficult to map the buffer set to the .NET shim so we copy the session key into a single buffer that can + // be freed by our shim. We need to make sure we clear out the remaining data set values and the set itself. + NetSecurityNative_MoveBuffer(&sessionKey->elements[0], outBuffer); + + for (i = 1; i < sessionKey->count; i++) + { + gss_release_buffer(&minorStatusFree, &sessionKey->elements[i]); + } + free(sessionKey->elements); + free(sessionKey); + + return majorStatus; +} + int32_t NetSecurityNative_EnsureGssInitialized(void) { #if defined(GSS_SHIM) diff --git a/src/native/libs/System.Net.Security.Native/pal_gssapi.h b/src/native/libs/System.Net.Security.Native/pal_gssapi.h index 10be636c7789e5..a72ec80eae7f5c 100644 --- a/src/native/libs/System.Net.Security.Native/pal_gssapi.h +++ b/src/native/libs/System.Net.Security.Native/pal_gssapi.h @@ -220,6 +220,14 @@ PALEXPORT uint32_t NetSecurityNative_GetUser(uint32_t* minorStatus, GssCtxId* contextHandle, PAL_GssBuffer* outBuffer); + +/* +Shims gss_inquire_sec_context_by_oid with GSS_C_INQ_SSPI_SESSION_KEY. +*/ +PALEXPORT uint32_t NetSecurityNative_InquireSecContextSessionKey(uint32_t* minorStatus, + GssCtxId* contextHandle, + PAL_GssBuffer* outBuffer); + /* Performs initialization of GSS shim, if necessary. Return value 0 indicates a success. diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index fc7332a54f9fac..1575d689d54798 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -1041,11 +1041,19 @@ check_include_files( HAVE_GSSFW_HEADERS) if (HAVE_GSSFW_HEADERS) + check_symbol_exists( + GSS_C_INQ_SSPI_SESSION_KEY + "GSS/GSS.h" + HAVE_GSS_C_INQ_SSPI_SESSION_KEY) check_symbol_exists( GSS_SPNEGO_MECHANISM "GSS/GSS.h" HAVE_GSS_SPNEGO_MECHANISM) else () + check_symbol_exists( + GSS_C_INQ_SSPI_SESSION_KEY, + "gssapi/gssapi.h" + HAVE_GSS_C_INQ_SSPI_SESSION_KEY) check_symbol_exists( GSS_SPNEGO_MECHANISM "gssapi/gssapi.h"