From 30cfc34c15bbe84094a0180a346d0d03e419c414 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sat, 30 Aug 2025 16:15:16 +0800 Subject: [PATCH 1/4] Init implement GSS API Authentication (gssapi-with-mic) --- ...piAuthenticationMethod.NegotiateContext.cs | 105 +++++++ ...icationMethod.ReflectedNegotiateContext.cs | 43 +++ .../GssApiAuthenticationMethod.cs | 266 ++++++++++++++++++ src/Renci.SshNet/KerberosCredential.cs | 55 ++++ .../Authentication/GssApiErrorMessage.cs | 73 +++++ .../Authentication/GssApiErrorTokenMessage.cs | 71 +++++ .../GssApiExchangeCompleteMessage.cs | 48 ++++ .../Authentication/GssApiMicMessage.cs | 73 +++++ .../Authentication/GssApiResponseMessage.cs | 71 +++++ .../Authentication/GssApiTokenMessage.cs | 69 +++++ .../Authentication/RequestMessageGssApi.cs | 63 +++++ src/Renci.SshNet/Session.cs | 28 ++ src/Renci.SshNet/SshMessageFactory.cs | 5 +- 13 files changed, 969 insertions(+), 1 deletion(-) create mode 100644 src/Renci.SshNet/GssApiAuthenticationMethod.NegotiateContext.cs create mode 100644 src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs create mode 100644 src/Renci.SshNet/GssApiAuthenticationMethod.cs create mode 100644 src/Renci.SshNet/KerberosCredential.cs create mode 100644 src/Renci.SshNet/Messages/Authentication/GssApiErrorMessage.cs create mode 100644 src/Renci.SshNet/Messages/Authentication/GssApiErrorTokenMessage.cs create mode 100644 src/Renci.SshNet/Messages/Authentication/GssApiExchangeCompleteMessage.cs create mode 100644 src/Renci.SshNet/Messages/Authentication/GssApiMicMessage.cs create mode 100644 src/Renci.SshNet/Messages/Authentication/GssApiResponseMessage.cs create mode 100644 src/Renci.SshNet/Messages/Authentication/GssApiTokenMessage.cs create mode 100644 src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs diff --git a/src/Renci.SshNet/GssApiAuthenticationMethod.NegotiateContext.cs b/src/Renci.SshNet/GssApiAuthenticationMethod.NegotiateContext.cs new file mode 100644 index 000000000..ebab8eff7 --- /dev/null +++ b/src/Renci.SshNet/GssApiAuthenticationMethod.NegotiateContext.cs @@ -0,0 +1,105 @@ +#if NET +using System; +using System.Buffers; +using System.Net; +using System.Net.Security; +#if NET8_0 +using System.Runtime.CompilerServices; +#endif +using System.Security.Principal; + +namespace Renci.SshNet +{ + public partial class GssApiAuthenticationMethod + { + private sealed class NegotiateContext : IAuthenticationContext + { +#if NET8_0 + // This API was made public in .NET 9 through ComputeIntegrityCheck. + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetMIC")] + private static extern void GetMICMethod(NegotiateAuthentication context, ReadOnlySpan data, IBufferWriter writer); +#endif + private readonly NegotiateAuthentication _negotiateAuthentication; + public NegotiateContext(bool delegateCredential, NetworkCredential credential, string targetName) + { + var negotiateOptions = new NegotiateAuthenticationClientOptions() + { + AllowedImpersonationLevel = delegateCredential ? TokenImpersonationLevel.Delegation : TokenImpersonationLevel.Impersonation, + Credential = credential, + Package = "Kerberos", + +#if NET10_0_OR_GREATER + RequiredProtectionLevel = ProtectionLevel.Sign, +#else + // While only Sign is needed we need to set EncryptAndSign for + // Windows client support. Sign only will pass in SECQOP_WRAP_NO_ENCRYPT + // to MakeSignature which fails. + // https://github.com/dotnet/runtime/issues/103461 + RequiredProtectionLevel = ProtectionLevel.EncryptAndSign, +#endif + + // While RFC states this should be set to "false", Win32-OpenSSH + // fails if it's not true. I'm unsure if openssh-portable on Linux + // will fail in the same way or not. + RequireMutualAuthentication = true, + TargetName = targetName + }; + + _negotiateAuthentication = new NegotiateAuthentication(negotiateOptions); + } + + public bool IsSigned + { + get + { + return _negotiateAuthentication.IsSigned; + } + } + + public byte[] ComputeIntegrityCheck(ReadOnlySpan message) + { + var signatureWriter = new ArrayBufferWriter(); +#if NET8_0 + GetMICMethod( + _negotiateAuthentication, + message, + signatureWriter); +#else + _negotiateAuthentication.ComputeIntegrityCheck( + message, + signatureWriter); +#endif + + return signatureWriter.WrittenSpan.ToArray(); + } + + public void Dispose() + { + _negotiateAuthentication.Dispose(); + } + + public byte[] GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateStatusCode statusCode) + { + var outgoingBlob = _negotiateAuthentication.GetOutgoingBlob(incomingBlob, out var code); + +#pragma warning disable IDE0010 // Add missing cases + switch (code) + { + case NegotiateAuthenticationStatusCode.ContinueNeeded: + statusCode = NegotiateStatusCode.ContinueNeeded; + break; + case NegotiateAuthenticationStatusCode.Completed: + statusCode = NegotiateStatusCode.Completed; + break; + default: + statusCode = NegotiateStatusCode.Other; + break; + } +#pragma warning restore IDE0010 // Add missing cases + + return outgoingBlob; + } + } + } +} +#endif diff --git a/src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs b/src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs new file mode 100644 index 000000000..d1aeea0ce --- /dev/null +++ b/src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs @@ -0,0 +1,43 @@ +#if !NET +#nullable enable +using System; +using System.Net; + +namespace Renci.SshNet +{ + public partial class GssApiAuthenticationMethod + { + private sealed class ReflectedNegotiateContext : IAuthenticationContext + { + +#pragma warning disable IDE0060 // Remove unused parameter + public ReflectedNegotiateContext(bool delegateCredential, NetworkCredential credential, string targetName) +#pragma warning restore IDE0060 // Remove unused parameter + { + } + + public bool IsSigned + { + get + { + throw new NotImplementedException(); + } + } + + public byte[] ComputeIntegrityCheck(ReadOnlySpan message) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + } + + public byte[] GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateStatusCode statusCode) + { + throw new NotImplementedException(); + } + } + } +} +#endif diff --git a/src/Renci.SshNet/GssApiAuthenticationMethod.cs b/src/Renci.SshNet/GssApiAuthenticationMethod.cs new file mode 100644 index 000000000..6bf2dce85 --- /dev/null +++ b/src/Renci.SshNet/GssApiAuthenticationMethod.cs @@ -0,0 +1,266 @@ +using System; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; + +using Renci.SshNet.Common; +using Renci.SshNet.Messages; +using Renci.SshNet.Messages.Authentication; +using Renci.SshNet.Messages.Transport; + +namespace Renci.SshNet +{ + /// + /// Provides functionality to perform GSS API authentication. + /// + public partial class GssApiAuthenticationMethod : AuthenticationMethod + { + // Kerberos - 1.2.840.113554.1.2.2 - This is DER encoding of the OID. + private static readonly byte[] KRB5OID = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x12, 0x01, 0x02, 0x02]; + + private readonly KerberosCredential _credential; + private readonly RequestMessageGssApi _requestMessage; + private readonly EventWaitHandle _mechanismSelectionCompleted = new AutoResetEvent(initialState: false); + private readonly EventWaitHandle _tokenExchangeCompleted = new AutoResetEvent(initialState: false); + private readonly EventWaitHandle _authenticationCompleted = new AutoResetEvent(initialState: false); + + private AuthenticationResult? _authenticationResult; +#pragma warning disable CA1859 // Use concrete types when possible for improved performance + private IAuthenticationContext _authenticationContext; +#pragma warning restore CA1859 // Use concrete types when possible for improved performance + private Session _session; + private bool _isDisposed; + + /// + /// Gets the name of the authentication method. + /// + public override string Name + { + get { return _requestMessage.MethodName; } + } + + /// + /// Initializes a new instance of the class. + /// + /// The username. + /// The . + public GssApiAuthenticationMethod(string username, KerberosCredential credential) + : base(username) + { + ThrowHelper.ThrowIfNull(credential); + + _credential = credential; + _requestMessage = new RequestMessageGssApi(ServiceName.Connection, Username, KRB5OID); + } + + /// + public override AuthenticationResult Authenticate(Session session) + { + ThrowHelper.ThrowIfNull(session); + _session = session; + + session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived; + session.UserAuthenticationGssApiResponseReceived += Session_UserAuthenticationGssApiResponseReceived; + session.RegisterMessage("SSH_MSG_USERAUTH_GSSAPI_RESPONSE"); + + try + { + session.SendMessage(_requestMessage); + session.WaitOnHandle(_mechanismSelectionCompleted); + } + finally + { + session.UnRegisterMessage("SSH_MSG_USERAUTH_GSSAPI_RESPONSE"); + session.UserAuthenticationGssApiResponseReceived -= Session_UserAuthenticationGssApiResponseReceived; + session.UserAuthenticationFailureReceived -= Session_UserAuthenticationFailureReceived; + } + + if (_authenticationResult.HasValue) + { + return _authenticationResult.Value; + } + + // RFC uses hostbased SPN format "service@host" but Windows SSPI needs the service/host format. + // .NET converts this format to the hostbased format expected by GSSAPI for us. + var targetName = !string.IsNullOrEmpty(_credential.TargetName) ? _credential.TargetName : $"host/{_session.ConnectionInfo.Host}"; + var networkCredential = _credential.NetworkCredential ?? CredentialCache.DefaultNetworkCredentials; +#if NET + _authenticationContext = new NegotiateContext(_credential.DelegateCredential, networkCredential, targetName); +#else + _authenticationContext = new ReflectedNegotiateContext(_credential.DelegateCredential, networkCredential, targetName); +#endif + var outgoingBlob = _authenticationContext.GetOutgoingBlob(Array.Empty(), out var statusCode); + var tokenMessage = new GssApiTokenMessage { Token = outgoingBlob }; + + session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived; + session.UserAuthenticationGssApiTokenReceived += Session_UserAuthenticationGssApiTokenReceived; + session.RegisterMessage("SSH_MSG_USERAUTH_GSSAPI_TOKEN"); + + try + { + session.SendMessage(tokenMessage); + session.WaitOnHandle(_tokenExchangeCompleted); + } + finally + { + session.UnRegisterMessage("SSH_MSG_USERAUTH_GSSAPI_TOKEN"); + session.UserAuthenticationGssApiTokenReceived -= Session_UserAuthenticationGssApiTokenReceived; + session.UserAuthenticationFailureReceived -= Session_UserAuthenticationFailureReceived; + } + + if (_authenticationResult.HasValue) + { + return _authenticationResult.Value; + } + + // While we request signing, the server may not so we need to check to see if we need to send a MIC. + Message finalExchangeMessage; + if (_authenticationContext.IsSigned) + { + var micDataStream = new SshDataStream(256); + micDataStream.WriteBinary(_session.SessionId); + micDataStream.WriteByte(RequestMessage.AuthenticationMessageCode); + micDataStream.Write(session.ConnectionInfo.Username, Encoding.UTF8); + micDataStream.Write("ssh-connection", Encoding.UTF8); + micDataStream.Write("gssapi-with-mic", Encoding.UTF8); + + var mic = _authenticationContext.ComputeIntegrityCheck(micDataStream.ToArray()); + + finalExchangeMessage = new GssApiMicMessage(mic); + } + else + { + finalExchangeMessage = new GssApiExchangeCompleteMessage(); + } + + session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived; + session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessReceived; + + try + { + session.SendMessage(finalExchangeMessage); + session.WaitOnHandle(_authenticationCompleted); + } + finally + { + session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessReceived; + session.UserAuthenticationFailureReceived -= Session_UserAuthenticationFailureReceived; + } + + return _authenticationResult.Value; + } + + private void Session_UserAuthenticationGssApiResponseReceived(object sender, MessageEventArgs e) + { + if (!KRB5OID.SequenceEqual(e.Message.SelectedMechanismOid)) + { + throw new SshConnectionException("The packet contains an unexpected value.", DisconnectReason.ProtocolError); + } + + _ = _mechanismSelectionCompleted.Set(); + } + + private void Session_UserAuthenticationGssApiTokenReceived(object sender, MessageEventArgs e) + { + var incomingBlob = e.Message.Token; + var outgoingBlob = _authenticationContext.GetOutgoingBlob(incomingBlob, out var statusCode); + + if (statusCode == NegotiateStatusCode.ContinueNeeded) + { + var tokenMessage = new GssApiTokenMessage { Token = outgoingBlob }; + _session.SendMessage(tokenMessage); + } + else if (statusCode == NegotiateStatusCode.Completed) + { + _ = _tokenExchangeCompleted.Set(); + } + else + { + throw new SshException($"Failed to generate the token. Status code: {statusCode}"); + } + } + + private void Session_UserAuthenticationSuccessReceived(object sender, MessageEventArgs e) + { + _authenticationResult = AuthenticationResult.Success; + _ = _authenticationCompleted.Set(); + } + + private void Session_UserAuthenticationFailureReceived(object sender, MessageEventArgs e) + { + if (e.Message.PartialSuccess) + { + _authenticationResult = AuthenticationResult.PartialSuccess; + } + else + { + _authenticationResult = AuthenticationResult.Failure; + } + + // Copy allowed authentication methods + AllowedAuthentications = e.Message.AllowedAuthentications; + + _ = _mechanismSelectionCompleted.Set(); + _ = _tokenExchangeCompleted.Set(); + _ = _authenticationCompleted.Set(); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + _authenticationContext?.Dispose(); + _mechanismSelectionCompleted.Dispose(); + _tokenExchangeCompleted.Dispose(); + _authenticationCompleted.Dispose(); + + _isDisposed = true; + } + + base.Dispose(disposing); + } + + /// + /// Represents a stateful authentication context. + /// + private interface IAuthenticationContext : IDisposable + { + /// + /// Gets a value indicating whether data signing was negotiated. + /// + bool IsSigned { get; } + + /// + /// Evaluates an authentication token sent by the other party and returns a token in response. + /// + /// Incoming authentication token, or empty value when initiating the authentication exchange. + /// Status code returned by the authentication provider. + /// An outgoing authentication token to be sent to the other party. + byte[] GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateStatusCode statusCode); + + /// + /// Computes the integrity check of a given message. + /// + /// Input message for MIC calculation. + /// The MIC. + byte[] ComputeIntegrityCheck(ReadOnlySpan message); + } + + private enum NegotiateStatusCode + { + ContinueNeeded, + Completed, + Other + } + } +} diff --git a/src/Renci.SshNet/KerberosCredential.cs b/src/Renci.SshNet/KerberosCredential.cs new file mode 100644 index 000000000..02df64262 --- /dev/null +++ b/src/Renci.SshNet/KerberosCredential.cs @@ -0,0 +1,55 @@ +#nullable enable +using System.Net; + +using Renci.SshNet.Common; + +namespace Renci.SshNet +{ + /// + /// Repesents credential for Kerberos authentication. + /// + public sealed class KerberosCredential + { + internal NetworkCredential? NetworkCredential { get; } + internal bool DelegateCredential { get; } + internal string? TargetName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The credential specified is used for the Kerberos authentication process. This can either be the same or + /// different from the username specified through ConnectionInfo.Username. The client settings username + /// is the target login user the SSH service is meant to run as, whereas the credential is the Kerberos + /// principal used for authentication. The rules for how a Kerberos principal maps to the target user is defined by + /// the SSH service itself. For example on Windows the username should be the same but on Linux the mapping can be + /// done through a .k5login file in the target user's home directory. + /// + /// If the credential is , the Kerberos authentication will be done using a cached ticket. + /// For Windows, this is the current thread's identity (typically logon user) will be used. + /// For Unix/Linux, this will use the Kerberos credential cache principal, which may be managed using the + /// kinit command. If there is no available cache credential, the authentication will fail. + /// + /// Credentials can only be delegated if the Kerberos ticket retrieved from the KDC is marked as forwardable. + /// Windows hosts will always retrieve a forwardable ticket but non-Windows hosts may not. When using an explicit + /// credential, make sure that 'forwardable = true' is set in the krb5.conf file so that .NET will request a + /// forwardable ticket required for delegation. When using a cached ticket, make sure that when the ticket was + /// retrieved it was retrieved with the forwardable flag. If the ticket is not forwardable, the authentication will + /// still work but the ticket will not be delegated. + /// + /// The credentials to use for the Kerberos authentication exchange. Set to null to use a cached ticket. + /// Allows the SSH server to delegate the user on remote systems. + /// Override the service principal name (SPN), default uses host/<ConnectionInfo.Host>. + public KerberosCredential(NetworkCredential? credential = null, bool delegateCredential = false, string? targetName = null) + { + if (!string.IsNullOrWhiteSpace(credential?.UserName)) + { + ThrowHelper.ThrowIfNullOrEmpty(credential!.Password); + } + + NetworkCredential = credential; + DelegateCredential = delegateCredential; + TargetName = targetName; + } + } +} diff --git a/src/Renci.SshNet/Messages/Authentication/GssApiErrorMessage.cs b/src/Renci.SshNet/Messages/Authentication/GssApiErrorMessage.cs new file mode 100644 index 000000000..d3030488e --- /dev/null +++ b/src/Renci.SshNet/Messages/Authentication/GssApiErrorMessage.cs @@ -0,0 +1,73 @@ +using System; +namespace Renci.SshNet.Messages.Authentication +{ + /// + /// Represents SSH_MSG_USERAUTH_GSSAPI_ERROR message. + /// + internal sealed class GssApiErrorMessage : Message + { + /// + public override string MessageName + { + get + { + return "SSH_MSG_USERAUTH_GSSAPI_ERROR"; + } + } + + /// + public override byte MessageNumber + { + get + { + return 64; + } + } + + /// + /// Gets the major status. + /// + public uint MajorStatus { get; private set; } + + /// + /// Gets the minor status. + /// + public uint MinorStatus { get; private set; } + + /// + /// Gets the message encoded in UTF8. + /// + public string Message { get; private set; } + + /// + /// Gets the language tag encoded in ASCII. + /// + public string LanguageTag { get; private set; } + + /// + /// Called when type specific data need to be loaded. + /// + protected override void LoadData() + { + MajorStatus = ReadUInt32(); + MinorStatus = ReadUInt32(); + + // The message text MUST be encoded in the UTF-8 encoding + Message = ReadString(Utf8); + LanguageTag = ReadString(Ascii); + } + + /// + /// Called when type specific data need to be saved. + /// + protected override void SaveData() + { + throw new NotImplementedException(); + } + + internal override void Process(Session session) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Renci.SshNet/Messages/Authentication/GssApiErrorTokenMessage.cs b/src/Renci.SshNet/Messages/Authentication/GssApiErrorTokenMessage.cs new file mode 100644 index 000000000..b0480294f --- /dev/null +++ b/src/Renci.SshNet/Messages/Authentication/GssApiErrorTokenMessage.cs @@ -0,0 +1,71 @@ +using System; + +namespace Renci.SshNet.Messages.Authentication +{ + /// + /// Represents SSH_MSG_USERAUTH_GSSAPI_ERRTOK message. + /// + internal sealed class GssApiErrorTokenMessage : Message + { + /// + public override string MessageName + { + get + { + return "SSH_MSG_USERAUTH_GSSAPI_ERRTOK"; + } + } + + /// + public override byte MessageNumber + { + get + { + return 65; + } + } + + /// + /// Gets the GSS token. + /// + public byte[] Token { get; private set; } + + /// + /// Gets the size of the message in bytes. + /// + /// + /// The size of the messages in bytes. + /// + protected override int BufferCapacity + { + get + { + var capacity = base.BufferCapacity; + capacity += 4; // GSS token length + capacity += Token.Length; // GSS token + return capacity; + } + } + + /// + /// Called when type specific data need to be loaded. + /// + protected override void LoadData() + { + Token = ReadBinary(); + } + + /// + /// Called when type specific data need to be saved. + /// + protected override void SaveData() + { + throw new NotImplementedException(); + } + + internal override void Process(Session session) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Renci.SshNet/Messages/Authentication/GssApiExchangeCompleteMessage.cs b/src/Renci.SshNet/Messages/Authentication/GssApiExchangeCompleteMessage.cs new file mode 100644 index 000000000..e1b519244 --- /dev/null +++ b/src/Renci.SshNet/Messages/Authentication/GssApiExchangeCompleteMessage.cs @@ -0,0 +1,48 @@ +using System; + +namespace Renci.SshNet.Messages.Authentication +{ + /// + /// Represents SSH_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE message. + /// + internal sealed class GssApiExchangeCompleteMessage : Message + { + /// + public override string MessageName + { + get + { + return "SSH_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE"; + } + } + + /// + public override byte MessageNumber + { + get + { + return 63; + } + } + + /// + /// Called when type specific data need to be loaded. + /// + protected override void LoadData() + { + throw new NotImplementedException(); + } + + /// + /// Called when type specific data need to be saved. + /// + protected override void SaveData() + { + } + + internal override void Process(Session session) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Renci.SshNet/Messages/Authentication/GssApiMicMessage.cs b/src/Renci.SshNet/Messages/Authentication/GssApiMicMessage.cs new file mode 100644 index 000000000..9f0f6d5e3 --- /dev/null +++ b/src/Renci.SshNet/Messages/Authentication/GssApiMicMessage.cs @@ -0,0 +1,73 @@ +using System; + +namespace Renci.SshNet.Messages.Authentication +{ + /// + /// Represents SSH_MSG_USERAUTH_GSSAPI_MIC message. + /// + internal sealed class GssApiMicMessage : Message + { + /// + public override string MessageName + { + get + { + return "SSH_MSG_USERAUTH_GSSAPI_MIC"; + } + } + + /// + public override byte MessageNumber + { + get + { + return 66; + } + } + + /// + /// Gets the MIC. + /// + public byte[] MIC + { + get; private set; + } + + protected override int BufferCapacity + { + get + { + var capacity = base.BufferCapacity; + capacity += 4; // MIC length + capacity += MIC.Length; // MIC + return capacity; + } + } + + public GssApiMicMessage(byte[] mic) + { + MIC = mic; + } + + /// + /// Called when type specific data need to be loaded. + /// + protected override void LoadData() + { + MIC = ReadBinary(); + } + + /// + /// Called when type specific data need to be saved. + /// + protected override void SaveData() + { + WriteBinaryString(MIC); + } + + internal override void Process(Session session) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Renci.SshNet/Messages/Authentication/GssApiResponseMessage.cs b/src/Renci.SshNet/Messages/Authentication/GssApiResponseMessage.cs new file mode 100644 index 000000000..f6b9d408c --- /dev/null +++ b/src/Renci.SshNet/Messages/Authentication/GssApiResponseMessage.cs @@ -0,0 +1,71 @@ +using System; + +namespace Renci.SshNet.Messages.Authentication +{ + /// + /// Represents SSH_MSG_USERAUTH_GSSAPI_RESPONSE message. + /// + internal sealed class GssApiResponseMessage : Message + { + /// + public override string MessageName + { + get + { + return "SSH_MSG_USERAUTH_GSSAPI_RESPONSE"; + } + } + + /// + public override byte MessageNumber + { + get + { + return 60; + } + } + + /// + /// Gets the selected mechanism OID. + /// + public byte[] SelectedMechanismOid { get; private set; } + + /// + /// Gets the size of the message in bytes. + /// + /// + /// The size of the messages in bytes. + /// + protected override int BufferCapacity + { + get + { + var capacity = base.BufferCapacity; + capacity += 4; // Selected mechanism oid length + capacity += SelectedMechanismOid.Length; // Selected mechanism oid + return capacity; + } + } + + /// + /// Called when type specific data need to be loaded. + /// + protected override void LoadData() + { + SelectedMechanismOid = ReadBinary(); + } + + /// + /// Called when type specific data need to be saved. + /// + protected override void SaveData() + { + throw new NotImplementedException(); + } + + internal override void Process(Session session) + { + session.OnUserAuthenticationGssApiResponseReceived(this); + } + } +} diff --git a/src/Renci.SshNet/Messages/Authentication/GssApiTokenMessage.cs b/src/Renci.SshNet/Messages/Authentication/GssApiTokenMessage.cs new file mode 100644 index 000000000..0de8f5eff --- /dev/null +++ b/src/Renci.SshNet/Messages/Authentication/GssApiTokenMessage.cs @@ -0,0 +1,69 @@ +namespace Renci.SshNet.Messages.Authentication +{ + /// + /// Represents SSH_MSG_USERAUTH_GSSAPI_TOKEN message. + /// + internal sealed class GssApiTokenMessage : Message + { + /// + public override string MessageName + { + get + { + return "SSH_MSG_USERAUTH_GSSAPI_TOKEN"; + } + } + + /// + public override byte MessageNumber + { + get + { + return 61; + } + } + + /// + /// Gets or sets the GSS token. + /// + public byte[] Token { get; set; } + + /// + /// Gets the size of the message in bytes. + /// + /// + /// The size of the messages in bytes. + /// + protected override int BufferCapacity + { + get + { + var capacity = base.BufferCapacity; + capacity += 4; // GSS token length + capacity += Token.Length; // GSS token + return capacity; + } + } + + /// + /// Called when type specific data need to be loaded. + /// + protected override void LoadData() + { + Token = ReadBinary(); + } + + /// + /// Called when type specific data need to be saved. + /// + protected override void SaveData() + { + WriteBinaryString(Token); + } + + internal override void Process(Session session) + { + session.OnUserAuthenticationGssApiTokenReceived(this); + } + } +} diff --git a/src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs b/src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs new file mode 100644 index 000000000..67306f7cd --- /dev/null +++ b/src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs @@ -0,0 +1,63 @@ +namespace Renci.SshNet.Messages.Authentication +{ + /// + /// Represents "gssapi-with-mic" SSH_MSG_USERAUTH_REQUEST message. + /// + internal sealed class RequestMessageGssApi : RequestMessage + { + private readonly byte[][] _supportedMechanismOids; + + /// + /// Gets the size of the message in bytes. + /// + /// + /// The size of the messages in bytes. + /// + protected override int BufferCapacity + { + get + { + var capacity = base.BufferCapacity; + if (_supportedMechanismOids?.Length > 0) + { + capacity += 4; // mechanism count length + foreach (var oid in _supportedMechanismOids) + { + capacity += oid.Length; // mechanism + } + } + + return capacity; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the service. + /// Authentication username. + /// The supported mechanism oids. + public RequestMessageGssApi(ServiceName serviceName, string username, params byte[][] supportedMechanismOids) + : base(serviceName, username, "gssapi-with-mic") + { + _supportedMechanismOids = supportedMechanismOids; + } + + /// + /// Called when type specific data need to be saved. + /// + protected override void SaveData() + { + base.SaveData(); + + if (_supportedMechanismOids?.Length > 0) + { + Write((uint)_supportedMechanismOids.Length); + foreach (var oid in _supportedMechanismOids) + { + WriteBinaryString(oid); + } + } + } + } +} diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index ec3eac878..235b164e2 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -401,6 +401,16 @@ public string ClientVersion /// internal event EventHandler> UserAuthenticationPublicKeyReceived; + /// + /// Occurs when message is received from the server. + /// + internal event EventHandler> UserAuthenticationGssApiResponseReceived; + + /// + /// Occurs when message is received from the server. + /// + internal event EventHandler> UserAuthenticationGssApiTokenReceived; + /// /// Occurs when message is received from the server. /// @@ -1690,6 +1700,24 @@ internal void OnUserAuthenticationPublicKeyReceived(PublicKeyMessage message) UserAuthenticationPublicKeyReceived?.Invoke(this, new MessageEventArgs(message)); } + /// + /// Called when message received. + /// + /// message. + internal void OnUserAuthenticationGssApiResponseReceived(GssApiResponseMessage message) + { + UserAuthenticationGssApiResponseReceived?.Invoke(this, new MessageEventArgs(message)); + } + + /// + /// Called when message received. + /// + /// message. + internal void OnUserAuthenticationGssApiTokenReceived(GssApiTokenMessage message) + { + UserAuthenticationGssApiTokenReceived?.Invoke(this, new MessageEventArgs(message)); + } + /// /// Called when message received. /// diff --git a/src/Renci.SshNet/SshMessageFactory.cs b/src/Renci.SshNet/SshMessageFactory.cs index 038d7c3ae..c75e182cb 100644 --- a/src/Renci.SshNet/SshMessageFactory.cs +++ b/src/Renci.SshNet/SshMessageFactory.cs @@ -56,7 +56,10 @@ internal sealed class SshMessageFactory new MessageMetadata(29, "SSH_MSG_KEXDH_REPLY", 31), new MessageMetadata(30, "SSH_MSG_KEX_DH_GEX_REPLY", 33), new MessageMetadata(31, "SSH_MSG_KEX_ECDH_REPLY", 31), - new MessageMetadata(32, "SSH_MSG_KEX_HYBRID_REPLY", 31) + new MessageMetadata(32, "SSH_MSG_KEX_HYBRID_REPLY", 31), + new MessageMetadata(33, "SSH_MSG_USERAUTH_GSSAPI_RESPONSE", 60), + new MessageMetadata(34, "SSH_MSG_USERAUTH_GSSAPI_TOKEN", 61), + new MessageMetadata(35, "SSH_MSG_USERAUTH_GSSAPI_ERRTOK", 65) }; private static readonly Dictionary MessagesByName = CreateMessagesByNameMapping(); From 86a38b8fd681ecb2e3bcd6057a060bb7884028cf Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Mon, 1 Sep 2025 21:22:15 +0800 Subject: [PATCH 2/4] Fix RequestMessageGssApi's BufferCapacity --- src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs b/src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs index 67306f7cd..a437f930a 100644 --- a/src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs +++ b/src/Renci.SshNet/Messages/Authentication/RequestMessageGssApi.cs @@ -23,6 +23,7 @@ protected override int BufferCapacity capacity += 4; // mechanism count length foreach (var oid in _supportedMechanismOids) { + capacity += 4; // mechanism length capacity += oid.Length; // mechanism } } From 3c7ffed4ceab9bb0310d1e56fd101b6a72499212 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Fri, 5 Sep 2025 22:25:48 +0800 Subject: [PATCH 3/4] Fail auth if init blob is null --- src/Renci.SshNet/GssApiAuthenticationMethod.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Renci.SshNet/GssApiAuthenticationMethod.cs b/src/Renci.SshNet/GssApiAuthenticationMethod.cs index 6bf2dce85..4f99b9463 100644 --- a/src/Renci.SshNet/GssApiAuthenticationMethod.cs +++ b/src/Renci.SshNet/GssApiAuthenticationMethod.cs @@ -91,6 +91,12 @@ public override AuthenticationResult Authenticate(Session session) _authenticationContext = new ReflectedNegotiateContext(_credential.DelegateCredential, networkCredential, targetName); #endif var outgoingBlob = _authenticationContext.GetOutgoingBlob(Array.Empty(), out var statusCode); + + if (outgoingBlob == null) + { + return AuthenticationResult.Failure; + } + var tokenMessage = new GssApiTokenMessage { Token = outgoingBlob }; session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived; From 37251d9b9c235b1921ff60e87d99354830962a40 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Fri, 5 Sep 2025 22:27:11 +0800 Subject: [PATCH 4/4] Fix StyleCop issue --- .../GssApiAuthenticationMethod.ReflectedNegotiateContext.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs b/src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs index d1aeea0ce..1d429552c 100644 --- a/src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs +++ b/src/Renci.SshNet/GssApiAuthenticationMethod.ReflectedNegotiateContext.cs @@ -9,7 +9,6 @@ public partial class GssApiAuthenticationMethod { private sealed class ReflectedNegotiateContext : IAuthenticationContext { - #pragma warning disable IDE0060 // Remove unused parameter public ReflectedNegotiateContext(bool delegateCredential, NetworkCredential credential, string targetName) #pragma warning restore IDE0060 // Remove unused parameter