Description
Background and motivation
The NegotiateAuthentication class is a nice manager wrapper around SSPI/GSSAPI and deals with all the API differences and platform quirks to be able to authenticate using Negotiate/Kerberos/NTLM auth and also sign/seal messages after authentication. Some protocols do not use the builtin signing/sealing mechanisms that is done through SSPI/GSSAPI but instead uses the GSSAPI "session key". An example of this is SMB where after authenticating it derives the encryption/signing key from that key https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/7fd079ca-17e6-4f02-8449-46b606ea289c
Session.SessionKey MUST be set to the first 16 bytes of the cryptographic key queried from the GSS protocol for this authenticated context. If the cryptographic key is less than 16 bytes, it is right-padded with zero bytes. If Connection.Dialect is “3.1.1” and Connection.CipherId is AES-256-CCM or AES-256-GCM, Session.FullSessionKey MUST be set to the cryptographic key as queried from the GSS protocol for this authenticated context. For information about how this is calculated for Kerberos authentication using Generic Security Service Application Programming Interface (GSS-API), see [MS-KILE] section 3.1.1.2. For information about how this is calculated for NTLM authentication using GSS-API, see [MS-NLMP] section 3.1.5.1.
If I want to implement my own SMB, or other protocol, implementation that uses this key in .NET I cannot use NegotiateAuthentication
because it doesn't expose any public way of retrieving the session key. I'll have to setup my own PInvokes and call GSSAPI/SSPI myself.
To retrieve this key on SSPI you would call QueryContextAttributesW with ulAttribute
being SECPKG_ATTR_SESSION_KEY
(9
) and a pBuffer
being a pointer to SecPkgContext_SessionKey. Once copied or no longer needed the SecPkgContext_SessionKey.SessionKey
is passed to FreeContextBuffer to free the memory.
To retrieve this key on GSSAPI you would call gss_inquire_sec_context_by_oid with the OID GSS_C_INQ_SSPI_SESSION_KEY
(1.2.840.113554.1.2.2.5.5
). Once copied or no longer needed the data_set
is passed to gss_release_buffer_set to free the memory. While gss_inquire_sec_context_by_oid
is an extension method it has been part of the three major GSSAPI implementations since:
- MIT krb5 - since 1.7 in 2009
- Heimdal - since 1.5 in 2011
- GSS.Framework (macOS) - See above, was forked after Heimdal 1.5
API Proposal
public partial class NegotiateAuthentication
{
// Original API here for historical purposes
// public byte[] GetSessionKey();
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);
public TReturn DeriveKeyFromSessionKey<TState, TReturn>(Func<ReadOnlySpan<byte>, TState, TReturn> keyDerivationFunction, TState state);
}
API Usage
var a = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions());
// Perform authentication
while (!a.IsAuthenticated)
{
a.GetOutgoingBlob(...);
...
}
// After authentication, retrieve session key
byte[] sessionKey = a.GetSessionKey();
// Derive signing key from session key based on MS-SMB2
Span<byte> signingKey = stackallock byte[16];
using SP800108HmacCounterKdf smb3kdf = new(key, HashAlgorithmName.SHA256);
smb3kdf.DeriveKey(
"SMBSigningKey\0"u8,
preauthHash, // Hash of preceding SMB messages
signingKey);
Alternative Designs
A potential alternative is to have the caller pass in a Span<byte>
and instead of returning the byte[]
the code copies the session key to the provided span if large enough.
public partial class NegotiateAuthentication
{
public void GetSessionKey(Span<byte> destination);
public bool TryGetSessionKey(Span<byte> destination, out int bytesWritten);
}
A downside to this approach is that I'm unsure if the key size can be queried from GSSAPI/SSPI and that the sizes may change depending on the environment/protocol used. For example NTLM will always stay at 16 bytes but Kerberos is based on the etype used, i.e 32 bytes for an AES256 based etype but 16 bytes for an AES128 based etype.
Risks
The main risk is really just expanding the support area of SSPI/GSSAPI. The documentation should stress that this key is not normally meant to be used directly and is exposed for compatibility with implementations that require this key like SMB.