[API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

Description

@filipnavara

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:

  1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
  2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
  3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
  4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

API Proposal

Wrap/Unwrap (signing and encryption)

Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

Impersonation level / mutual authentication

The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
_ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
...}
...

Alternative Designs

KDC proxy support and server-side validation

Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    [API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

    Description

    @filipnavara

    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:

    1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
    2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
    3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
    4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

    API Proposal

    Wrap/Unwrap (signing and encryption)

    Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

    namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

    Impersonation level / mutual authentication

    The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

    namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

    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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

    publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

    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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

    message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
    _ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
    ...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
    ...}
    ...

    Alternative Designs

    KDC proxy support and server-side validation

    Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

    namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

    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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

    Metadata

    Metadata

    Assignees

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      [API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

      Description

      @filipnavara

      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:

      1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
      2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
      3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
      4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

      API Proposal

      Wrap/Unwrap (signing and encryption)

      Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

      namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

      Impersonation level / mutual authentication

      The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

      namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

      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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

      publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

      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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

      message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
      _ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
      ...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
      ...}
      ...

      Alternative Designs

      KDC proxy support and server-side validation

      Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

      namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

      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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

      Metadata

      Metadata

      Assignees

      Labels

      api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

      Type

      No type

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        [API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

        Description

        @filipnavara

        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:

        1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
        2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
        3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
        4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

        API Proposal

        Wrap/Unwrap (signing and encryption)

        Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

        namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

        Impersonation level / mutual authentication

        The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

        namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

        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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

        publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

        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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

        message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
        _ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
        ...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
        ...}
        ...

        Alternative Designs

        KDC proxy support and server-side validation

        Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

        namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

        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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

        Metadata

        Metadata

        Assignees

        Labels

        api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          [API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

          Description

          @filipnavara

          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:

          1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
          2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
          3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
          4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

          API Proposal

          Wrap/Unwrap (signing and encryption)

          Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

          namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

          Impersonation level / mutual authentication

          The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

          namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

          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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

          publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

          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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

          message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
          _ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
          ...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
          ...}
          ...

          Alternative Designs

          KDC proxy support and server-side validation

          Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

          namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

          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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

          Metadata

          Metadata

          Assignees

          Labels

          api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

          Type

          No type

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            [API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

            Description

            @filipnavara

            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:

            1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
            2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
            3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
            4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

            API Proposal

            Wrap/Unwrap (signing and encryption)

            Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

            namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

            Impersonation level / mutual authentication

            The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

            namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

            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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

            publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

            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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

            message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
            _ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
            ...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
            ...}
            ...

            Alternative Designs

            KDC proxy support and server-side validation

            Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

            namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

            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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

            Metadata

            Metadata

            Assignees

            Labels

            api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

              Description

              @filipnavara

              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:

              1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
              2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
              3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
              4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

              API Proposal

              Wrap/Unwrap (signing and encryption)

              Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

              namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

              Impersonation level / mutual authentication

              The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

              namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

              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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

              publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

              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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

              message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
              _ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
              ...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
              ...}
              ...

              Alternative Designs

              KDC proxy support and server-side validation

              Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

              namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

              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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

              Metadata

              Metadata

              Assignees

              Labels

              api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

              Type

              No type

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                [API Proposal]: Add Wrap and Unwrap methods to NegotiateAuthentication API #70909

                Description

                @filipnavara

                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:

                1. Implementation of NTLM and Negotiate SASL authentication mechanisms as used by SMTP, IMAP, LDAP and other protocols. This is currently done internally in the SmtpClient class. Additionally it would be useful for external libraries like MailKit for identical scenarios.
                2. Bi-directional authenticated communication between two parties after the initial authentication was negotiated. This is what NegotiateStream does today but also what projects like .NET/C# client for Apache Kudu need.
                3. Specifying the allowed impersonation (eg. delegation) for the credentials used by the client. Thus is necessary prerequisite to enable scenarios for SQL Server client library.
                4. Handling server-side scenarios with KDC proxy (Kerberos Domain Controller exposed on HTTPS endpoint for external authentication).

                API Proposal

                Wrap/Unwrap (signing and encryption)

                Additional methods are added that closely reflect the GSS_Wrap and GSS_Unwrap methods in the GSSAPI specification. They map most of the functionality save for the qop input parameter which was never exposed by the internal APIs in .NET and which is commonly set to 0 (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. The IBufferWriter<byte> interface seems to convey these semantics quite cleanly. For Unwrap 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.

                namespaceSystem.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>publicsealedclassNegotiateAuthentication: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="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>publicNegotiateAuthenticationStatusCodeWrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,refboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrap(ReadOnlySpan<byte>input,IBufferWriter<byte>outputWriter,outboolisEncrypted);/// <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>publicNegotiateAuthenticationStatusCodeUnwrapInplace(Span<byte>input,outintunwrappedOffset,outintunwrappedLength,outboolisEncrypted);}

                Impersonation level / mutual authentication

                The internal NTAuthentication API used ContextFlagsPal 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 the NegotiateAuthenticationClientOptions and NegotiateAuthentication with additional properties that facilitate scenarios related to impersonation and mutual authentication.

                namespaceSystem.Net.Security;publicclassNegotiateAuthenticationClientOptions{/// <summary>/// Indicates that mutual authentication is required between the client and server./// </summary>publicboolRequireMutualAuthentication{get;set;}/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating how the server/// can use the client's credentials to access resources./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelAllowedImpersonationLevel{get;set;}}publicclassNegotiateAuthentication{/// <summary>/// One of the <see cref="TokenImpersonationLevel" /> values, indicating the negotiated/// level of impresonation./// </summary>publicSystem.Security.Principal.TokenImpersonationLevelImpersonationLevel{get;}}

                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 and ASC_REQ_PROXY_BINDINGS flags used for different scenarios. On managed side these are exposed by the ExtendedProtectionPolicy 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 and RequiredImpersonationLevel to the server-side options and moves the validation responsibility into the NegotiateAuthentication 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 either TargetUnknown or BadBinding. Similarly, the ImpersonationLevel would be validated against RequiredImpresonationLevel and return the ImpersonationValidationFailed 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 and HttpListener with almost identical code that could be shared.

                publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates extended security and validation policies./// </summary>publicSystem.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>publicSystem.Security.Principal.TokenImpersonationLevelRequiredImpersonationLevel{get;set;}}publicenumNegotiateAuthenticationStatusCode{/// <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,}

                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 into NegotiateAuthenticationServerOptions. Instead of performing manual validation after the handshake an error code would be directly returned from the GetOutgoingBlob API and transformed into appropriate error response for the caller:

                message=negotiateAuthentication.GetOutgoingBlob(message,outvarstatusCode);// Simplified validation:if(statusCode==NegotiateAuthenticationStatusCode.BadBinding||statusCode==NegotiateAuthenticationStatusCode.TargetUnknown||statusCode==NegotiateAuthenticationStatusCode.ImpersonationValidationFailed||statusCode==NegotiateAuthenticationStatusCode.SecurityQosFailed){exception=statusCodeswitch{NegotiateAuthenticationStatusCode.BadBinding=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.TargetUnknown=>newAuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch),NegotiateAuthenticationStatusCode.ImpersonationValidationFailed=>newAuthenticationException(SR.Format(SR.net_auth_context_expectation,_expectedImpersonationLevel.ToString(),PrivateImpersonationLevel.ToString())),
                _ =>// NegotiateAuthenticationStatusCode.SecurityQosFailedexception=newAuthenticationException(SR.Format(SR.net_auth_context_expectation,result.ToString(),_expectedProtectionLevel.ToString())),};intstatusCode=ERROR_TRUST_FAILURE;message=newbyte[sizeof(long)];BinaryPrimitives.WriteInt64LittleEndian(message,statusCode);awaitSendAuthResetSignalAndThrowAsync<TIOAdapter>(message,exception,cancellationToken).ConfigureAwait(false);Debug.Fail("Unreachable");}elseif(statusCode==NegotiateAuthenticationStatusCode.Completed){// Signal remote party that we are done
                ...}elseif(statusCode!=NegotiateAuthenticationStatusCode.ContinueNeeded){// Signal remote side on a failed attempt.
                ...}
                ...

                Alternative Designs

                KDC proxy support and server-side validation

                Instead of exposing the full ExtendedProtectionPolicy as NegotiateAuthenticationServerOptions.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.

                namespaceSystem.Net.Security;publicenumNegotiateAuthenticationBindingValidation{/// <summary>/// Check the channel binding against the value in transport./// </summary>Default,/// <summary>/// Allow missing channel binding on the transport./// </summary>/// <remarks>Maps to the ASC_REQ_ALLOW_MISSING_BINDINGS SSPI flag.</remarks>AllowMissingBindings,/// <summary>/// Require channel binding but don't check its value./// </summary>/// <remarks>Maps to the ASC_REQ_PROXY_BINDINGS SSPI flag.</remarks>ProxyBindings,}publicclassNegotiateAuthenticationServerOptions{/// <summary>/// Indicates the required level of channel binding validation./// </summary>publicNegotiateAuthenticationBindingValidationBindingValidation{get;set;}}

                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 in NegotiateStream/HttpListener and thus make it easier for the caller to implement any of the advanced validation scenarios correctly.

                Metadata

                Metadata

                Assignees

                Labels

                api-approvedAPI was approved in API review, it can be implementedarea-System.Net.SecurityblockingMarks issues that we want to fast track in order to unblock other important work

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions