Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: Expose a high level authentication API #69920

Closed
wfurt opened this issue May 27, 2022 · 32 comments · Fixed by #70720
Closed

[API Proposal]: Expose a high level authentication API #69920

wfurt opened this issue May 27, 2022 · 32 comments · Fixed by #70720
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Net.Security blocking Marks issues that we want to fast track in order to unblock other important work
Milestone

Comments

@wfurt
Copy link
Member

wfurt commented May 27, 2022

Background and motivation

runtime already have very similar implementation - used primarily by SocketsHttphandler and partially by SMTP.
Kestrel currently use this internal API via Reflection (#29270). Additionally, we would like to have similar authentication in Android handler that lives outside of runtime repo.

Tha main goal is to expose existing functionality via public API so other components can leverage it as well.

API Proposal

namespace System.Net.Security
{
 public class NegotiateAuthenticationClientOptions
 {
     // Specifies the GSSAPI authentication package used for the authentication.
     // Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate.
     public string Package { get; set; }

     // The NetworkCredential that is used to establish the identity of the client.
     // Default value is CrendentialCache.DefaultCredential.
     public NetworkCredential Credential { get; set; }

     // The Service Principal Name (SPN) that uniquely identifies the server to authenticate.
     public string? TargetName { get; set; }

     // The ChannelBinding that is used for extended protection.
     public ChannelBinding? Binding { get; set; }

     // Indicates the requires level of protection of the authentication exchange
     // and any further data exchange.
     // Valid values: None, Sign, EncryptAndSign
     // Default value: None
     System.Net.Security.ProtectionLevel RequiredProtectionLevel { get; set; }
 }

 public class NegotiateAuthenticationServerOptions
 {
    // Specifies the GSSAPI authentication package used for the authentication.
    // Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate.
    public string Package { get; set; }

    // The NetworkCredential that is used to establish the identity of the client.
    // Default value is CrendentialCache.DefaultCredential.
    // Note: I'm not quite sure of the meaning of this on the server side but
    // it exists on GSSAPI side.
    public NetworkCredential Credential { get; set; }

    // The ChannelBinding that is used for extended protection.
    public ChannelBinding? Binding { get; set; }

    // Indicates the requires level of protection of the authentication exchange
    // and any further data exchange.
    // Valid values: None, Sign, EncryptAndSign
    // Default value: None
    System.Net.Security.ProtectionLevel RequiredProtectionLevel { get; set; }
 }

 public class NegotiateAuthentication : IDisposable
 {
    // Create client-side authentication
    public NegotiateAuthentication(NegotiateAuthenticationClientOptions clientOptions);

    // Create server-side authentication
    public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOptions);

    // Indicates whether the initial authentication finished.
    // NOTE: Original it was named IsCompleted in the proposal but I renamed
    // it to match the property on NegotiateStream.
    public bool IsAuthenticated { get; set; }

    // Indicates negotiated protection level (can be higher than the required one).
    // Returns None if authentication was not finished yet.
    public System.Net.Security.ProtectionLevel ProtectionLevel { get; set; }

    // Indicates whether signing was negotiated.
    // Returns false if authentication was not finished yet.
    public bool IsSigned { get; set; }

    // Indicates whether signing was negotiated.
    // Returns false if authentication was not finished yet.
    public bool IsEncrypted { get; set; }

    // Indicates whether the client and server are mutually authenticated.
    // Returns false if authentication was not finished yet.
    public bool IsMutuallyAuthenticated { get; set; }

    // Indicates whether this is server-side authentication context.
    // Returns value based on the constructor used, provided for parity with
    // NegoatiateStream.
    public bool IsServer { get; set; }

    // The GSSAPI package name that was used for the authentiation. Initially it's
    // the name specified in the options in constructor. After successful authentication
    // it should return the actual mechanism used. For example, if Negotiate is
    // specified as the requested GSSAPI package this may return NTLM or Kerberos once
    // authentication exchange is complete (eg. IsAuthenticated == true).
    //
    // NOTE: This is partly duplicate with RemoteIdentity so it may not be necessary
    // on the public API.
    public string Package { get; }

    // For server context it returns the SPN target name of the client after successful
    // authentication. For client context it returns the target name specified in the
    // constructor options.
    public string? TargetName { get; }

    // Gets information about the identity of the remote party.
    //
    // When accessed by the client, this property returns a GenericIdentity containing
    // the Service Principal Name (SPN) of the server and the authentication protocol used.
    //
    // Throws InvalidOperationException if authentication was not finished yet
    // (IsAuthenticated == false) for server-side.
    public System.Security.Principal.IIdentity RemoteIdentity { get };

    public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode);

    // Base64 version of GetOutgoingBlob
    public string GetOutgoingBlob(string incomingBlob, out NegotiateAuthenticationStatusCode statusCode);

    // TODO (APIs not necessary for HTTP authentication but necessary for high-level protocols
    // like SASL and NegotiateStream):
    // Wrap, Unwrap as replacement for Encrypt, Decrypt, MakeSignature and VerifySignature
    // GetMIC and VerifyMIC if we find a use for that
 }

 // NOTE: Mirrors SecurityStatusPalErrorCode at the moment but it should mostly map to GSSAPI error
 // codes.
 public enum NegotiateAuthenticationStatusCode
 {
    NotSet = 0,
    OK,
    ContinueNeeded,
    CompleteNeeded,
    CompleteAndContinue,
    ContextExpired,
    CredentialsNeeded,
    Renegotiate,
    TryAgain,

    // Errors
    OutOfMemory,
    InvalidHandle,
    Unsupported,
    TargetUnknown,
    InternalError,
    PackageNotFound,
    NotOwner,
    CannotInstall,
    InvalidToken,
    CannotPack,
    QopNotSupported,
    NoImpersonation,
    LogonDenied,
    UnknownCredentials,
    NoCredentials,
    MessageAltered,
    OutOfSequence,
    NoAuthenticatingAuthority,
    IncompleteMessage,
    IncompleteCredentials,
    BufferNotEnough,
    WrongPrincipal,
    TimeSkew,
    UntrustedRoot,
    IllegalMessage,
    CertUnknown,
    CertExpired,
    DecryptFailure,
    AlgorithmMismatch,
    SecurityQosFailed,
    SmartcardLogonRequired,
    UnsupportedPreauth,
    BadBinding,
    DowngradeDetected,
    ApplicationProtocolMismatch,
    NoRenegotiation
 }
}

API Usage

var auth = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions("NTLM", testCredential "HTTP/foo"));
NegotiateAuthenticationStatusCode statusCode;

byte[]? negotiateBlob = ntAuth.GetOutgoingBlob(ReadOnlySpan<byte>.Empty, out NegotiateAuthenticationStatusCode statusCode);
sendBlob(negotiateBlob);
do
{
  buffer = receiveBlob();
  byte[]? negotiateBlob = ntAuth.GetOutgoingBlob(buffer, out statusCode);
} while (statusCode == NegotiateAuthenticationStatusCode .ContinueNeeded);

if (statusCode == NegotiateAuthenticationStatusCode.OK)
{
 ....
}
else
{
...
}

Alternative Designs

No response

Risks

This API currently works well for bot client and server side of HTTP. We really have weak test coverage for SMTP and other protocols.

@wfurt wfurt added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Net.Security labels May 27, 2022
@wfurt wfurt added this to the 7.0.0 milestone May 27, 2022
@wfurt wfurt self-assigned this May 27, 2022
@ghost
Copy link

ghost commented May 27, 2022

Tagging subscribers to this area: @dotnet/ncl, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

runtime already have very similar implementation - used primarily by SocketsHttphandler and partially by SMTP.
Kestrel currently use this internal API via Reflection (#29270). Additionally, we would like to have similar authentication in Android handler that lives outside of runtime repo.

Tha main goal is to expose existing functionality via public API so other components can leverage it as well.

API Proposal

namespace System.Net.Security
{
 public class NegotiateAuthenticationClientOptions
 {
     // Specifies the GSSAPI authentication package used for the authentication.
     // Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate.
     public string Package { get; set; }

     // The NetworkCredential that is used to establish the identity of the client.
     // Default value is CrendentialCache.DefaultCredential.
     public NetworkCredential Credential { get; set; }

     // The Service Principal Name (SPN) that uniquely identifies the server to authenticate.
     public string? TargetName { get; set; }

     // The ChannelBinding that is used for extended protection.
     public ChannelBinding? Binding { get; set; }

     // Indicates the requires level of protection of the authentication exchange
     // and any further data exchange.
     // Valid values: None, Sign, EncryptAndSign
     // Default value: None
     System.Net.Security.ProtectionLevel RequiredProtectionLevel { get; set; }
 }

 public class NegotiateAuthenticationServerOptions
 {
    // Specifies the GSSAPI authentication package used for the authentication.
    // Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate.
    public string Package { get; set; }

    // The NetworkCredential that is used to establish the identity of the client.
    // Default value is CrendentialCache.DefaultCredential.
    // Note: I'm not quite sure of the meaning of this on the server side but
    // it exists on GSSAPI side.
    public NetworkCredential Credential { get; set; }

    // The ChannelBinding that is used for extended protection.
    public ChannelBinding? Binding { get; set; }

    // Indicates the requires level of protection of the authentication exchange
    // and any further data exchange.
    // Valid values: None, Sign, EncryptAndSign
    // Default value: None
    System.Net.Security.ProtectionLevel RequiredProtectionLevel { get; set; }
 }

 public class NegotiateAuthentication : IDisposable
 {
    // Create client-side authentication
    public NegotiateAuthentication(NegotiateAuthenticationClientOptions clientOptions);

    // Create server-side authentication
    public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOptions);

    // Indicates whether the initial authentication finished.
    // NOTE: Original it was named IsCompleted in the proposal but I renamed
    // it to match the property on NegotiateStream.
    public bool IsAuthenticated { get; set; }

    // Indicates negotiated protection level (can be higher than the required one).
    // Returns None if authentication was not finished yet.
    public System.Net.Security.ProtectionLevel ProtectionLevel { get; set; }

    // Indicates whether signing was negotiated.
    // Returns false if authentication was not finished yet.
    public bool IsSigned { get; set; }

    // Indicates whether signing was negotiated.
    // Returns false if authentication was not finished yet.
    public bool IsEncrypted { get; set; }

    // Indicates whether the client and server are mutually authenticated.
    // Returns false if authentication was not finished yet.
    public bool IsMutuallyAuthenticated { get; set; }

    // Indicates whether this is server-side authentication context.
    // Returns value based on the constructor used, provided for parity with
    // NegoatiateStream.
    public bool IsServer { get; set; }

    // The GSSAPI package name that was used for the authentiation. Initially it's
    // the name specified in the options in constructor. After successful authentication
    // it should return the actual mechanism used. For example, if Negotiate is
    // specified as the requested GSSAPI package this may return NTLM or Kerberos once
    // authentication exchange is complete (eg. IsAuthenticated == true).
    //
    // NOTE: This is partly duplicate with RemoteIdentity so it may not be necessary
    // on the public API.
    public string Package { get; }

    // For server context it returns the SPN target name of the client after successful
    // authentication. For client context it returns the target name specified in the
    // constructor options.
    public string? TargetName { get; }

    // Gets information about the identity of the remote party.
    //
    // When accessed by the client, this property returns a GenericIdentity containing
    // the Service Principal Name (SPN) of the server and the authentication protocol used.
    //
    // Throws InvalidOperationException if authentication was not finished yet
    // (IsAuthenticated == false) for server-side.
    public System.Security.Principal.IIdentity RemoteIdentity { get };

    public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode);

    // Base64 version of GetOutgoingBlob
    public string GetOutgoingBlob(string incomingBlob, out NegotiateAuthenticationStatusCode statusCode);

    // TODO (APIs not necessary for HTTP authentication but necessary for high-level protocols
    // like SASL and NegotiateStream):
    // Wrap, Unwrap as replacement for Encrypt, Decrypt, MakeSignature and VerifySignature
    // GetMIC and VerifyMIC if we find a use for that
 }

 // NOTE: Mirrors SecurityStatusPalErrorCode at the moment but it should mostly map to GSSAPI error
 // codes.
 public enum NegotiateAuthenticationStatusCode
 {
    NotSet = 0,
    OK,
    ContinueNeeded,
    CompleteNeeded,
    CompleteAndContinue,
    ContextExpired,
    CredentialsNeeded,
    Renegotiate,
    TryAgain,

    // Errors
    OutOfMemory,
    InvalidHandle,
    Unsupported,
    TargetUnknown,
    InternalError,
    PackageNotFound,
    NotOwner,
    CannotInstall,
    InvalidToken,
    CannotPack,
    QopNotSupported,
    NoImpersonation,
    LogonDenied,
    UnknownCredentials,
    NoCredentials,
    MessageAltered,
    OutOfSequence,
    NoAuthenticatingAuthority,
    IncompleteMessage,
    IncompleteCredentials,
    BufferNotEnough,
    WrongPrincipal,
    TimeSkew,
    UntrustedRoot,
    IllegalMessage,
    CertUnknown,
    CertExpired,
    DecryptFailure,
    AlgorithmMismatch,
    SecurityQosFailed,
    SmartcardLogonRequired,
    UnsupportedPreauth,
    BadBinding,
    DowngradeDetected,
    ApplicationProtocolMismatch,
    NoRenegotiation
 }
}

API Usage

var auth = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions("NTLM", testCredential "HTTP/foo"));
NegotiateAuthenticationStatusCode statusCode;

byte[]? negotiateBlob = ntAuth.GetOutgoingBlob(ReadOnlySpan<byte>.Empty, out NegotiateAuthenticationStatusCode statusCode);
sendBlob(negotiateBlob);
do
{
  buffer = receiveBlob();
  byte[]? negotiateBlob = ntAuth.GetOutgoingBlob(buffer, out statusCode);
} while (statusCode == NegotiateAuthenticationStatusCode .ContinueNeeded);

if (statusCode == NegotiateAuthenticationStatusCode.OK)
{
 ....
}
else
{
...
}

### Alternative Designs

_No response_

### Risks

This API currently works well for bot client and server side of HTTP. We really have weak test coverage for SMTP and other protocols. 

<table>
  <tr>
    <th align="left">Author:</th>
    <td>wfurt</td>
  </tr>
  <tr>
    <th align="left">Assignees:</th>
    <td>wfurt</td>
  </tr>
  <tr>
    <th align="left">Labels:</th>
    <td>

`api-suggestion`, `area-System.Net.Security`

</td>
  </tr>
  <tr>
    <th align="left">Milestone:</th>
    <td>7.0.0</td>
  </tr>
</table>
</details>

@wfurt wfurt added blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 27, 2022
@davidfowl
Copy link
Member

public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode);

Can we make a non-allocating API as well?

@filipnavara
Copy link
Member

Can we make a non-allocating API as well?

It's non-trivial, unfortunately. GetOutgoingBlob mutates the internal state and there's no easy way to know how big buffer you would need. That prevents using the common Try*+Span patterns used elsewhere. It may be possible to make a version that has optimistic non-allocating code path for the buffer although some internal allocations are likely still going to happen. I am open to suggestions.

@wfurt
Copy link
Member Author

wfurt commented May 28, 2022

There is pattern in SslStream where you can pass in buffer via ref and if big enough it would be used, if not, new one would be allocated. I don't know if that is worth of the try but I agree the rest is difficult. We we would go would go down that path it would be

NegotiateAuthenticationStatusCode  GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob,ref byte[]? outgoingBlob);

@filipnavara
Copy link
Member

There is pattern in SslStream where you can pass in buffer via ref and if big enough it would be used, if not, new one would be allocated. I don't know if that is worth of the try but I agree the rest is difficult. We we would go would go down that path it would be

NegotiateAuthenticationStatusCode  GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob,ref byte[]? outgoingBlob);

I am honestly not sure if it's worth it. Another idea would be to return a simple disposable struct. That way it would be possible to pool the buffer and it would make it possible to clear the buffer contents during Dispose too. It's additional complexity but it would make it slightly less error-prone to ensure that some sensitive data are not kept around.

@wfurt
Copy link
Member Author

wfurt commented May 28, 2022

yah. I'm not sure either.

@filipnavara
Copy link
Member

filipnavara commented May 28, 2022

Apparently the concept in my proposal above already exists as the IMemoryOwner<T> interface, so we can have

public IMemoryOwner<byte> GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode);

and do the pooling.

@davidfowl
Copy link
Member

I guess I should ask: How often is this API called?

@filipnavara
Copy link
Member

filipnavara commented May 28, 2022

I guess I should ask: How often is this API called?

On each Negotiate/NTLM authenticated connection about 3-4 times for NTLM, less for Kerberos. That is true both for the server side (Kestrel) and client side (HttpClient). The authentication is session based, ie. authenticated connection could be reused for multiple requests to the same server and authentication overhead is present only for the first request.

The buffers will likely never be big enough to spill to large object heap and given the usage patterns they are likely to live only on gen 0 GC.

@davidfowl
Copy link
Member

OK I'm convinced, for now 😄.

cc @mconnew

@mconnew
Copy link
Member

mconnew commented May 28, 2022

This will work for WCF and CoreWCF

@SteveSyfuhs
Copy link

The buffers will likely never be big enough to spill to large object heap and given the usage patterns they are likely to live only on gen 0 GC.

What number triggers the LOH?

By default on Windows we have a MaxTokenSize of 48000 bytes with a max of 65535 bytes. Customers set this when users have more than a few hundred groups (taps out at 1000), so lots of folks set this. In delegation scenarios there are two tokens involved (targeted service ticket + TGT), so double that max size for what's moving across the wire, and since it's moving across the wire it will be base64 encoded so +40% overhead.

NegotiateAuthenticationClientOptions

Ninor nit. Negotiate is its own specific package. If you're letting callers pass their own package name then it's not doing negotiate. Suggest GssApiAuthentication or NetworkAuthentication or even PlatformAuthentication.

NegotiateAuthentication : IDisposable

It would be quite nice if this were its own interface, or at least have a mechanism to pass another internal implementation. Ideally NetworkCredential Credential is an interface type as well because NetworkCredential only supports passwords.

@filipnavara
Copy link
Member

What number triggers the LOH?

85000 bytes iirc. This is the non-base64 size for this particular API.

@filipnavara
Copy link
Member

filipnavara commented Jun 1, 2022

NegotiateAuthenticationClientOptions

Minor nit. Negotiate is its own specific package. If you're letting callers pass their own package name then it's not doing negotiate. Suggest GssApiAuthentication or NetworkAuthentication or even PlatformAuthentication.

The naming is difficult and I am open to suggestions. The original internal class was named NTAuthentication which is not really great, and for reason stated below (reflection on internal API) cannot be used as a public API name. I would be fine with GssApi instead of Negotiate in the class names. It's only slightly inaccurate since on Windows it's called SSPI but otherwise there's a lot going for it. Conceptually SSPI is the same as GSSAPI anyway. NetworkAuthentication sounds really too generic to me, especially since in many IETF protocols the network authentication is done through the SASL standard, or some other one (HTTP auth, OAuth). PlatformAuthentication is also too generic and doesn't really imply that it's Negotiate/Kerberos/NTLM which are cross-platform standards.

NegotiateAuthentication : IDisposable

It would be quite nice if this were its own interface, or at least have a mechanism to pass another internal implementation. Ideally NetworkCredential Credential is an interface type as well because NetworkCredential only supports passwords.

The idea was to expose single API to consumers but possibly use some internal interface as implementation. Currently there's two backends - the platform API one (GSSAPI / SSPI) and the managed one (NTLM + Negotiate protocols in managed code, no Kerberos). We may eventually explore integrating the managed Kerberos implementation, or combining the different backends (eg. managed Negotiate and NTLM but Kerberos handled through platform APIs). That is, however, not necessarily focus at the moment. We want to unblock scenarios where the current internal NTAuthentication API is used. In dotnet/runtime itself the implementation is copied into at least three libraries now with no binary code sharing. Kestrel consumes one of the implementation through reflection. Other components (MS SQL client, Android HTTP handler, CoreWCF) all use different ways to consume the non-public APIs. Some 3rd-party libraries just reimplement it, each with different set of bugs. While I would love to have a grand design with pluggable implementations (like GSSAPI does) it's explicitly not a goal for the first iteration.

@filipnavara
Copy link
Member

Ideally NetworkCredential Credential is an interface type as well because NetworkCredential only supports passwords.

Passwords and single sign-on (default credentials).

@SteveSyfuhs
Copy link

it's only slightly inaccurate since on Windows it's called SSPI

SSPI is GSS-like, and when we (the SSPI-owning team 😀) discuss things on the wire we generically refer to it as GSS so not the worst option. We also have historically referred to SSPI/GSS as 'platform authentication mechanisms' since most OS platforms don't support anything beyond these. Could potentially go with OSPlatformAuthentication but that's getting wordy.

While I would love to have a grand design with pluggable implementations (like GSSAPI does) it's explicitly not a goal for the first iteration.

Fair enough.

Passwords and single sign-on (default credentials).

Yes, my point was moreso that it doesn't support other platform credentials like certificates or keytabs. Alternative option is to extend NetworkCredential to be more generic too, but I imagine that also falls into the not-MVP bucket.

@filipnavara
Copy link
Member

Alternative option is to extend NetworkCredential to be more generic too, but I imagine that also falls into the not-MVP bucket.

NetworkCredential is not sealed, so extending later in this direction should be possible if the need arises (but sticking to MVP here to move it along).

@terrajobst
Copy link
Member

terrajobst commented Jun 2, 2022

Video

  • Looks generally good
  • We considered a "less visible" namespace, but System.Net.Security is already fairly advanced, so this feels it belongs there.
  • We considered making the options get; init; but since the constructors logically copy, there is no point.
  • NegotiateAuthentication
    • Should either be sealed or implement the Dispose pattern. Given that nothing is virtual, sealing seems more appropriate.
    • A lot of the properties are settable; it seems they all should get-only as they are merely indicate where the internal state machine is at.
  • NegotiateAuthenticationStatusCode
    • Consider trimming it down to the errors listed in the spec and/or the ones we need/think are important and treat the remainder as a pass through.
namespace System.Net.Security;

public class NegotiateAuthenticationClientOptions
{
    public string Package { get; set; }
    public NetworkCredential Credential { get; set; }
    public string? TargetName { get; set; }
    public ChannelBinding? Binding { get; set; }
    ProtectionLevel RequiredProtectionLevel { get; set; }
}
public class NegotiateAuthenticationServerOptions
{
    public string Package { get; set; }
    public NetworkCredential Credential { get; set; }
    public ChannelBinding? Binding { get; set; }
    ProtectionLevel RequiredProtectionLevel { get; set; }
}
public sealed class NegotiateAuthentication : IDisposable
{
    public NegotiateAuthentication(NegotiateAuthenticationClientOptions clientOptions);
    public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOptions);
    public bool IsAuthenticated { get; }
    public ProtectionLevel ProtectionLevel { get; }
    public bool IsSigned { get; }
    public bool IsEncrypted { get; }
    public bool IsMutuallyAuthenticated { get; }
    public bool IsServer { get; }
    public string Package { get; }
    public string? TargetName { get; }
    public IIdentity RemoteIdentity { get };
    public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode);
    public string GetOutgoingBlob(string incomingBlob, out NegotiateAuthenticationStatusCode statusCode);
}
public enum NegotiateAuthenticationStatusCode
{
    NotSet = 0,
    OK,
    ContinueNeeded,
    CompleteNeeded,
    CompleteAndContinue,
    ContextExpired,
    CredentialsNeeded,
    Renegotiate,
    TryAgain,
    OutOfMemory,
    InvalidHandle,
    Unsupported,
    TargetUnknown,
    InternalError,
    PackageNotFound,
    NotOwner,
    CannotInstall,
    InvalidToken,
    CannotPack,
    QopNotSupported,
    NoImpersonation,
    LogonDenied,
    UnknownCredentials,
    NoCredentials,
    MessageAltered,
    OutOfSequence,
    NoAuthenticatingAuthority,
    IncompleteMessage,
    IncompleteCredentials,
    BufferNotEnough,
    WrongPrincipal,
    TimeSkew,
    UntrustedRoot,
    IllegalMessage,
    CertUnknown,
    CertExpired,
    DecryptFailure,
    AlgorithmMismatch,
    SecurityQosFailed,
    SmartCardLogonRequired,
    UnsupportedPreauth,
    BadBinding,
    DowngradeDetected,
    ApplicationProtocolMismatch,
    NoRenegotiation
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jun 2, 2022
@davidfowl
Copy link
Member

So I know this API got approved and I wasn't in the meeting but based on the conversation about the non allocating API and the usage, it seems like GetOutgoingBlob is called per request https://github.com/dotnet/aspnetcore/blob/d02dca7d2285090888fa18bd7789c94504c97191/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs#L130.

This means we have an allocation per request right?

@filipnavara
Copy link
Member

This means we have an allocation per request right?

To me it seems the state is kept across requests on the same connection. Subsequent requests should already be in the .IsCompleted state and the API should be non-allocating in that case.

@filipnavara
Copy link
Member

I started working on an implementation here: https://github.com/dotnet/runtime/compare/main...filipnavara:negotiate-api?expand=1 (very rough prototype at the moment)

@xqrzd
Copy link

xqrzd commented Jun 3, 2022

Would Wrap/Unwrap be implemented in the future? I use NegotiateStream to do Kerberos authentication for an Apache Kudu Client, and part of the authentication is exchanging Kerberos protected messages. Without those methods I'm stuck on NegotiateStream.
https://github.com/xqrzd/kudu-client-net/blob/main/src/Knet.Kudu.Client/Negotiate/Negotiator.cs#L321

@SteveSyfuhs
Copy link

This means we have an allocation per request right?

To me it seems the state is kept across requests on the same connection.

Each unauthenticated request will have a token payload and each partnered response will also have a token response payload until the handshake is complete (hey -> ack, hey2 -> ack2, etc.). This is usually at most 4 roundtrips, but FYI we have some things coming up in Windows that will push this higher as we move to deprecate and kill NTLM. Once the handshake is complete the session is bound to the TCP connection (or whatever persistence is used -- i.e. whatever holds the security context handle for SSPI or GSS) and then there is nothing else going across the wire auth-wise. This will repeat again once the persistence mechanism has closed the handle (TCP closed, switched to different frontend, etc.).

@filipnavara
Copy link
Member

filipnavara commented Jun 3, 2022

Would Wrap/Unwrap be implemented in the future?

Likely yes. The plan was to do minimum viable prototype to cover HTTP authentication first, then follow up from there.

@terrajobst
Copy link
Member

@davidfowl

This means we have an allocation per request right?

My understanding was that this API will be called to authenticate a user. Once that is done, the payload is stored by the networking API (say HttpClient) and simply put in a header on subsequent calls. So the allocation would happen once per session, rather than once per request.

@filipnavara
Copy link
Member

filipnavara commented Jun 4, 2022

I went through the error codes and culled it. There were lot of unrelated SChannel/TLS error codes. I basically reworked it starting with the GSSAPI specification:

    public enum NegotiateAuthenticationStatusCode
    {
        Completed = 0, // GSS_S_COMPLETE
        ContinueNeeded, // GSS_S_CONTINUE_NEEDED

        GenericFailure, // GSS_S_FAILURE/GSS_S_NO_CONTEXT
        BadBinding, // GSS_S_BAD_BINDINGS
        Unsupported, // GSS_S_BAD_MECH (Unsupported mechanism)
        MessageAltered, // GSS_S_BAD_SIG = GSS_S_BAD_MIC
        ContextExpired, // GSS_S_CONTEXT_EXPIRED
        CredentialsExpired, // GSS_S_CREDENTIALS_EXPIRED
        InvalidCredentials, // GSS_S_DEFECTIVE_CREDENTIAL
        InvalidToken, // GSS_S_DEFECTIVE_TOKEN
        UnknownCredentials, // GSS_S_NO_CRED
        QopNotSupported, // GSS_S_BAD_QOP
        OutOfSequence, // GSS_S_DUPLICATE_TOKEN/GSS_S_OLD_TOKEN/GSS_S_UNSEQ_TOKEN/GSS_S_GAP_TOKEN + GSS_E_FAILURE

        // GSSAPI statuses not mapped:
        //
        // GSS_S_BAD_STATUS - API misuse
        // GSS_S_UNAVAILABLE - context import/export, not exposed
        // GSS_S_DUPLICATE_ELEMENT - API misuse
        // GSS_S_NAME_NOT_MN - not exposed
        // GSS_S_BAD_NAME - API misuse, we only use known name formats
        // GSS_S_BAD_NAMETYPE - API misuse, we only use known name types
        // GSS_S_UNAUTHORIZED - not exposed
    }

with the following mapping from the internal SecurityStatusPalErrorCode:

            statusCode = securityStatus.ErrorCode switch
            {
                SecurityStatusPalErrorCode.NotSet => NegotiateAuthenticationStatusCode.GenericFailure,

                SecurityStatusPalErrorCode.OK => NegotiateAuthenticationStatusCode.Completed,
                SecurityStatusPalErrorCode.ContinueNeeded => NegotiateAuthenticationStatusCode.ContinueNeeded,

                // These code should never be returned and they should be handled internally
                SecurityStatusPalErrorCode.CompleteNeeded => NegotiateAuthenticationStatusCode.Completed,
                SecurityStatusPalErrorCode.CompAndContinue => NegotiateAuthenticationStatusCode.ContinueNeeded,

                SecurityStatusPalErrorCode.ContextExpired => NegotiateAuthenticationStatusCode.ContextExpired,
                SecurityStatusPalErrorCode.Unsupported => NegotiateAuthenticationStatusCode.Unsupported,
                SecurityStatusPalErrorCode.TargetUnknown => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.InternalError => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.PackageNotFound => NegotiateAuthenticationStatusCode.Unsupported,
                SecurityStatusPalErrorCode.NotOwner => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.CannotInstall => NegotiateAuthenticationStatusCode.Unsupported,
                SecurityStatusPalErrorCode.InvalidToken => NegotiateAuthenticationStatusCode.InvalidToken,
                SecurityStatusPalErrorCode.CannotPack => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.QopNotSupported => NegotiateAuthenticationStatusCode.QopNotSupported,
                SecurityStatusPalErrorCode.NoImpersonation => NegotiateAuthenticationStatusCode.UnknownCredentials,
                SecurityStatusPalErrorCode.LogonDenied => NegotiateAuthenticationStatusCode.UnknownCredentials,
                SecurityStatusPalErrorCode.UnknownCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials,
                SecurityStatusPalErrorCode.NoCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials,
                SecurityStatusPalErrorCode.MessageAltered => NegotiateAuthenticationStatusCode.MessageAltered,
                SecurityStatusPalErrorCode.OutOfSequence => NegotiateAuthenticationStatusCode.OutOfSequence,
                SecurityStatusPalErrorCode.NoAuthenticatingAuthority => NegotiateAuthenticationStatusCode.InvalidCredentials,
                SecurityStatusPalErrorCode.IncompleteCredentials => NegotiateAuthenticationStatusCode.InvalidCredentials,
                SecurityStatusPalErrorCode.IllegalMessage => NegotiateAuthenticationStatusCode.InvalidToken,
                SecurityStatusPalErrorCode.CertExpired => NegotiateAuthenticationStatusCode.CredentialsExpired,
                SecurityStatusPalErrorCode.SecurityQosFailed => NegotiateAuthenticationStatusCode.QopNotSupported,
                SecurityStatusPalErrorCode.SmartcardLogonRequired => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.UnsupportedPreauth => NegotiateAuthenticationStatusCode.Unsupported,
                SecurityStatusPalErrorCode.BadBinding => NegotiateAuthenticationStatusCode.BadBinding,

                // API misuse or incorrect inputs
                SecurityStatusPalErrorCode.OutOfMemory => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.InvalidHandle => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.BufferNotEnough => NegotiateAuthenticationStatusCode.GenericFailure,
                // Processing partial inputs is not supported, so this is result of incorrect input
                SecurityStatusPalErrorCode.IncompleteMessage => NegotiateAuthenticationStatusCode.InvalidToken,

                // TLS related non-error codes => map to generic failure
                SecurityStatusPalErrorCode.CredentialsNeeded => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.Renegotiate => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.TryAgain => NegotiateAuthenticationStatusCode.GenericFailure,

                // TLS related error codes => map to generic failure
                SecurityStatusPalErrorCode.DowngradeDetected => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.ApplicationProtocolMismatch => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.NoRenegotiation => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.CertUnknown => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.DecryptFailure => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.AlgorithmMismatch => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.TimeSkew => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.UntrustedRoot => NegotiateAuthenticationStatusCode.GenericFailure,
                SecurityStatusPalErrorCode.WrongPrincipal => NegotiateAuthenticationStatusCode.GenericFailure,
            };

@teo-tsirpanis
Copy link
Contributor

If we want to minimize allocations we could expose something like the following:

public void WriteOutgoingBlob<TWriter>(ReadOnlySpan<byte> incomingBlob, TWriter outgoingBlob, out NegotiateAuthenticationStatusCode statusCode) where TWriter : IBufferWriter<byte>;

@davidfowl thoughts?

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jun 14, 2022
@filipnavara
Copy link
Member

filipnavara commented Jun 15, 2022

If we want to minimize allocations we could expose something like the following:

public void WriteOutgoingBlob<TWriter>(ReadOnlySpan<byte> incomingBlob, TWriter outgoingBlob, out NegotiateAuthenticationStatusCode statusCode) where TWriter : IBufferWriter<byte>;

@davidfowl thoughts?

Underappreciated suggestion :) I have been playing with the pattern for the Wrap/Unwrap APIs and I really like it. Maybe we can add it later with simplified prototype like this:

public NegotiateAuthenticationStatusCode WriteOutgoingBlob(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter);

@terrajobst
Copy link
Member

If we want to minimize allocations we could expose something like the following

Not sure it's worth it TBH. Authentication itself shouldn't happen that often. This would be a bigger concern if the outgoing blob would have to be retrieved on every request, which doesn't seem to be the case.

@filipnavara
Copy link
Member

If we want to minimize allocations we could expose something like the following

Not sure it's worth it TBH. Authentication itself shouldn't happen that often. This would be a bigger concern if the outgoing blob would have to be retrieved on every request, which doesn't seem to be the case.

Agreed for the authentication itself.

I want to run Wrap/Unwrap (formerly Encrypt/Decrypt and MakeSignature/VerifySignature) through API review later. These are the primitives that would power NegotiateStream.Read/Write type of scenarios (aside from SASL authentication) and where the allocations are more of a concern. At that point it may be reasonable to allow the same pattern (IBufferWriter) to be used for all the APIs. I don't expect the current GetOutgoingBlob to go away though.

@wfurt
Copy link
Member Author

wfurt commented Jun 16, 2022

You can probably start adding to #62202 @filipnavara. That was intended to cover any use beyond just the basic need. (and Kestrel reflection #29270)

@filipnavara
Copy link
Member

filipnavara commented Jun 16, 2022

@wfurt Will do. I'd like to get #70720 done first before moving further.

Here's a backlog of my API proposals based on experimenting with the new APIs to support additional scenarios:

  • Extend NegotiateAuthenticationServerOptions to support scenarios in HttpListener and potentially unify SPN validation on NTLM between HttpListener and NegotiateStream. This could be anything from two bool flags added (MVP) to adding ExtendedProtectionPolicy and unifying the SPN verification.
  • Add Wrap/Unwrap methods to support encryption and signing using the negotiated authentication. This unblock the new API for SMTP authentication and usage in NegotiateStream. It also covers scenario in [API Proposal]: Expose a high level authentication API #69920 (comment) and possibly something in SQL Client (to be checked).
  • Add WriteOutgoingBlob to achieve parity with Wrap/Unwrap in how buffers are used (provided that the API from previous step is approved).
  • Add GetMIC/VerifyMIC methods to cover the reminder of the basic GSSAPI calls. There are currently no external consumers for this but it would simplify testing and it is already used internally in the Negotiate protocol implementation. This would enable some internal refactoring later to use managed NTLM on Linux platforms where the native implementation is not available.

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jun 21, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Jul 21, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Net.Security blocking Marks issues that we want to fast track in order to unblock other important work
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants