-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909
Comments
Tagging subscribers to this area: @dotnet/ncl, @vcsjones Issue DetailsBackground and motivationIn #69920 the initial This follows with addition to the API surface to support two additional scenarios:
Two additional methods are added that closely reflect the API Proposalnamespace System.Net.Security;
/// <summary>
/// Represents a stateful authentication exchange that uses the Negotiate, NTLM or Kerberos security protocols
/// to authenticate the client or server, in client-server communication.
/// </summary>
public sealed class NegotiateAuthentication : IDisposable
{
/// <summary>
/// Wrap an input message with signature and optionally with an encryption.
/// <summary>
/// <param name="input">Input message to be wrapped.</param>
/// <param name="outputWriter">Buffer writter where the wrapped message is written.</param>
/// <param name="isConfidential">
/// On input specifies whether confidentiality transformation (encryption) is requested.
/// On output specifies whether the confidentiality transformation was applied in the wrapping.
/// </param>
/// <returns>
/// <see cref="NegotiateAuthenticationStatusCode.Completed" /> on success, other
/// <see cref="NegotiateAuthenticationStatusCode" /> values on failure.
/// </returns>
/// <remarks>
/// Like the <see href="https://datatracker.ietf.org/doc/html/rfc2743#page-65">GSS_Wrap</see> API
/// the authentication protocol implementation may choose to override the requested value in the
/// <see cref="isConfidential" /> parameter. This may result in either downgrade or upgrade of the
/// protection level.
/// </remarks>
public NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan<byte> input, IBufferWriter<byte> outputWriter, ref bool isConfidential);
/// <summary>
/// Unwrap an input message with signature or encryption applied by the other party.
/// <summary>
/// <param name="input">Input message to be unwrapped.</param>
/// <param name="outputWriter">Buffer writter where the unwrapped message is written.</param>
/// <param name="isConfidential">
/// On output specifies whether the wrapped message had confidentiality transformation applied.
/// </param>
/// <returns>
/// <see cref="NegotiateAuthenticationStatusCode.Completed" /> on success.
/// <see cref="NegotiateAuthenticationStatusCode.MessageAltered" /> if the message signature was
/// invalid.
/// <see cref="NegotiateAuthenticationStatusCode.InvalidToken" /> if the wrapped message was
/// in invalid format.
/// Other <see cref="NegotiateAuthenticationStatusCode" /> values on failure.
/// </returns>
public NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan<byte> input, IBufferWriter<byte> outputWriter, out bool isConfidential);
} API UsageSpan<byte> messageLengthBuffer = new byte[4];
NegotiateAuthentication auth = new NegotiateAuthentication(
new NegotiateAuthenticationClientOptions
{
Package = "NTLM",
Credential = s_testCredentialRight,
TargetName = "HTTP/foo",
RequiredProtectionLevel = ProtectionLevel.EncryptAndSign
});
// Do the initial authentication exchange through auth.GetOutgoingBlob
Authenticate(auth, stream);
// Exchange length-prefixed messages between client and server
ArrayBufferWriter<byte> output = new ArrayBufferWriter<byte>();
bool isConfidential = true;
NegotiateAuthenticationStatusCode statusCode;
// Encrypt and sign the word "hello"
statusCode = ntAuth.Wrap("hello"u8, output, ref isConfidential);
if (statusCode == NegotiateAuthenticationStatusCode.Completed)
{
BinaryPrimitives.WriteInt32LittleEndian(messageLengthBuffer, output.WrittenCount);
stream.Write(messageLengthBuffer);
stream.Write(output.WrittenSpan);
stream.ReadExactly(messageLengthBuffer);
int messageLength = BinaryPrimitives.ReadInt32LittleEndian(messageLengthBuffer);
// For simplicity I do allocations here but the APIs are designed to avoid them if desired
byte[] messageBuffer = new byte[messageLength];
stream.ReadExactly(messageBuffer);
output.Clear(); // Reuse buffer
statusCode = ntAuth.Unwrap(messageBuffer, output, out _);
if (statusCode == NegotiateAuthenticationStatusCode.Completed)
{
Console.WriteLine("Got reply: " + Encoding.UTF8.GetString(output.WrittenSpan));
}
} Alternative DesignsNo response RisksNo response
|
Draft implementation available at https://github.com/filipnavara/runtime/tree/negotiate-wrap-api (based on PR #70720 which implements the initial shape of cc @wfurt |
It looks like it would work. I'll give it a try when it's ready. |
I updated the proposal slightly to mention exceptions and rename the |
Triage: It would allow us to avoid Reflection in SqlClient. |
Mainly pending an API review to finalize the public API shape. I have the implementation mostly ready and it should be really quick to tweak it to the final API shape. This would be useful for us (eM Client) to drop private interop bindings too. |
Updated/rebased draft implementation at https://github.com/filipnavara/runtime/tree/negotiate-wrap-api2. |
namespace System.Net.Security;
/// <summary>
/// Represents a stateful authentication exchange that uses the Negotiate, NTLM or Kerberos security protocols
/// to authenticate the client or server, in client-server communication.
/// </summary>
public sealed class NegotiateAuthentication : IDisposable
{
/// <summary>
/// Wrap an input message with signature and optionally with an encryption.
/// </summary>
/// <param name="input">Input message to be wrapped.</param>
/// <param name="outputWriter">Buffer writer where the wrapped message is written.</param>
/// <param name="isEncrypted">
/// On input specifies whether encryption is requested.
/// On output specifies whether encryption was applied in the wrapping.
/// </param>
/// <returns>
/// <see cref="NegotiateAuthenticationStatusCode.Completed" /> on success, other
/// <see cref="NegotiateAuthenticationStatusCode" /> values on failure.
/// </returns>
/// <remarks>
/// Like the <see href="https://datatracker.ietf.org/doc/html/rfc2743#page-65">GSS_Wrap</see> API
/// the authentication protocol implementation may choose to override the requested value in the
/// isEncrypted parameter. This may result in either downgrade or upgrade of the protection level.
/// </remarks>
/// <exception cref="InvalidOperationException">Authentication failed or has not occurred.</exception>
public NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan<byte> input, IBufferWriter<byte> outputWriter, bool requestEncryption, out bool isEncrypted);
/// <summary>
/// Unwrap an input message with signature or encryption applied by the other party.
/// </summary>
/// <param name="input">Input message to be unwrapped.</param>
/// <param name="outputWriter">Buffer writter where the unwrapped message is written.</param>
/// <param name="isEncrypted">
/// On output specifies whether the wrapped message had encryption applied.
/// </param>
/// <returns>
/// <see cref="NegotiateAuthenticationStatusCode.Completed" /> on success.
/// <see cref="NegotiateAuthenticationStatusCode.MessageAltered" /> if the message signature was
/// invalid.
/// <see cref="NegotiateAuthenticationStatusCode.InvalidToken" /> if the wrapped message was
/// in invalid format.
/// Other <see cref="NegotiateAuthenticationStatusCode" /> values on failure.
/// </returns>
/// <exception cref="InvalidOperationException">Authentication failed or has not occurred.</exception>
public NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan<byte> input, IBufferWriter<byte> outputWriter, out bool isEncrypted);
/// <summary>
/// Unwrap an input message with signature or encryption applied by the other party.
/// </summary>
/// <param name="input">Input message to be unwrapped. On output contains the decoded data.</param>
/// <param name="unwrappedOffset">Offset in the input buffer where the unwrapped message was written.</param>
/// <param name="unwrappedLength">Length of the unwrapped message.</param>
/// <param name="isEncrypted">
/// On output specifies whether the wrapped message had encryption applied.
/// </param>
/// <returns>
/// <see cref="NegotiateAuthenticationStatusCode.Completed" /> on success.
/// <see cref="NegotiateAuthenticationStatusCode.MessageAltered" /> if the message signature was
/// invalid.
/// <see cref="NegotiateAuthenticationStatusCode.InvalidToken" /> if the wrapped message was
/// in invalid format.
/// Other <see cref="NegotiateAuthenticationStatusCode" /> values on failure.
/// </returns>
/// <exception cref="InvalidOperationException">Authentication failed or has not occurred.</exception>
public NegotiateAuthenticationStatusCode UnwrapInPlace(Span<byte> input, out int unwrappedOffset, out int unwrappedLength, out bool isEncrypted);
}
public partial class NegotiateAuthenticationClientOptions
{
/// <summary>
/// Indicates that mutual authentication is required between the client and server.
/// </summary>
public bool RequireMutualAuthentication { get; set; }
/// <summary>
/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server
/// can use the client's credentials to access resources.
/// </summary>
public System.Security.Principal.TokenImpersonationLevel AllowedImpersonationLevel { get; set; }
}
public partial class NegotiateAuthentication
{
/// <summary>
/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated
/// level of impresonation.
/// </summary>
public System.Security.Principal.TokenImpersonationLevel ImpersonationLevel { get; }
}
public partial class NegotiateAuthenticationServerOptions
{
/// <summary>
/// Indicates extended security and validation policies.
/// </summary>
public System.Security.Authentication.ExtendedProtectionPolicy? Policy { get; set; }
/// <summary>
/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server
/// can use the client's credentials to access resources.
/// </summary>
public System.Security.Principal.TokenImpersonationLevel RequiredImpersonationLevel { get; set; }
}
public partial enum NegotiateAuthenticationStatusCode
{
/// <status>Validation of RequiredProtectionLevel against negotiated protection level failed.</status>
/// <remarks>Part of original API proposal, just not enforced in the managed code yet</remarks>
SecurityQosFailed,
/// <status>Validation of the target name failed</status>
TargetUnknown,
/// <status>Validation of the impersonation level failed</status>
ImpersonationValidationFailed,
} |
I would be fine with changing it. However, ASP.NET already consumes the API and Android HTTP handler is in PR so if we do that we need to coordinate with them. One missing thing from the API review notes is that |
Background and motivation
In #69920 the initial
NegotiateAuthentication
API surface was reviewed and approved. This was intentionally scaled down to a minimum viable proposal to unblock client-side and server-side HTTP authentication scenarios without resorting to reflection on internal API surface.This follows with addition to the API surface to support additional scenarios:
NTLM
andNegotiate
SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in theSmtpClient
class. Additionally it would be useful for external libraries like MailKit for identical scenarios.NegotiateStream
does today but also what projects like .NET/C# client for Apache Kudu need.API Proposal
Wrap/Unwrap (signing and encryption)
Additional methods are added that closely reflect the
GSS_Wrap
andGSS_Unwrap
methods in the GSSAPI specification. They map most of the functionality save for theqop
input parameter which was never exposed by the internal APIs in .NET and which is commonly set to0
(default QOP protection) in the native APIs.The main problematic point is how to express the buffer allocation semantics in a concise way. For
Wrap
the size of the encrypted content is not known before hand and the operation modifies an internal context state so it needs to succeed in one go, ie. it cannot return "buffer too small" error and let the caller retry. We want to allow the caller to reuse the buffer as long as it is big enough. TheIBufferWriter<byte>
interface seems to convey these semantics quite cleanly. ForUnwrap
we know the unwrapped data are at most as big as the input wrapped data. On Windows we can efficiently do the unwrapping inline within the same buffer and it would be nice to expose it to the caller.Impersonation level / mutual authentication
The internal
NTAuthentication
API usedContextFlagsPal
enum to specify additional requests such as signing, encryption, or mutual authentication to the authentication provider. Exposing the enum itself on public API was deemed inappropriate because it was mixing flags for client-side and server-side authentication. Many of the flags were mutually exclusive or specific to Schannel (TLS) authentication not related to the new API. The minimum viable prototype was to expose the required protection level (none, signing, encryption and signing). This API suggestion extends theNegotiateAuthenticationClientOptions
andNegotiateAuthentication
with additional properties that facilitate scenarios related to impersonation and mutual authentication.KDC proxy support and server-side validation
In the KDC proxy scenario the problem is to specifying how channel binding validation is performed. The client connects to a HTTPS endpoint of the KDC proxy and thus the channel binding used in the authentication may not match what the server expects. In the native SSPI methods this is represented by the
ASC_REQ_ALLOW_MISSING_BINDINGS
andASC_REQ_PROXY_BINDINGS
flags used for different scenarios. On managed side these are exposed by theExtendedProtectionPolicy
class along with other policy options such as list of service principal names.There are two different ways to expose the underlying functionality. The minimum viable way is just directly exposing these two flags, it is described below in the Alternative Designs section. The proposal here adds
ExtendedProtectionPolicy
andRequiredImpersonationLevel
to the server-side options and moves the validation responsibility into theNegotiateAuthentication
class.The validation of channel bindings and target name (SPN), or collectively the extended security policy, would be moved into the last step of
GetOutgoingBlob
in the authentication flow. It would report the status back as eitherTargetUnknown
orBadBinding
. Similarly, theImpersonationLevel
would be validated againstRequiredImpresonationLevel
and return theImpersonationValidationFailed
error if the check fails.In Kerberos scenarios the SPN is already validated by the system libraries. This would simply align NTLM to expose the same level of validation but implemented in managed code. This is currently done in
NegotiateStream
andHttpListener
with almost identical code that could be shared.API Usage
On client-side the API usage would be identical to #69920 with the addition of the new options used for the authentication specified in
NegotiateAuthenticationClientOptions
.Server-side validation
On server-side the API usage would also be conceptually identical but the validation code would be changed slightly.
For example, within
NegotiateStream.AuthenticateAsServer[Async]
the extended protection policy would be passed intoNegotiateAuthenticationServerOptions
. Instead of performing manual validation after the handshake an error code would be directly returned from theGetOutgoingBlob
API and transformed into appropriate error response for the caller:Alternative Designs
KDC proxy support and server-side validation
Instead of exposing the full
ExtendedProtectionPolicy
asNegotiateAuthenticationServerOptions.Policy
only the underlying system flags could be exposed. The full validation of SPNs or impersonation level would be left to the caller. No new error codes would be introduced.Risks
Currently
NegotiateAuthentication
is pretty light-weight wrapper over GSSAPI (non-Windows) and SSPI (Windows) native APIs. Moving part of the server-side validation into the wrapper risks that something is implemented incorrectly. On the other hand, the expectation is to reuse the existing code already present inNegotiateStream
/HttpListener
and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.The text was updated successfully, but these errors were encountered: