Files
ssh.net/src/Renci.SshNet/ClientAuthentication.cs
T
mus65 4e02502bdf Add .NET 10 target and make use of C#14 extension members (#1672)
* Add .NET 10 target

* fix IDE0031

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0031

* fix ca5399

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca5399

* fix ca1515

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1515

* fix ca2002

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2002

* fix ca1508

new false positives.

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1508

* fix ca2000

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2000

* fix ca2025

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2025

* fix ca1849

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1849

* fix Reverse() overloads

because of https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/csharp-overload-resolution

* supress CA2002

* Use extension members for ThrowHelpers

* use extension members for CryptoAbstractions

* use extension member for DateTime.UnixEpoch

* use extension members for string.Join etc

* use extension members for Convert.To/FromHexString

* disable CA1508

* Update .NET 10 RC2

* Workaround Build Regression in .NET 10 RC2

https://github.com/dotnet/sdk/issues/51265

* suppress new warnings introduced by merge

* Update to .NET 10 final release

* Revert "Workaround Build Regression in .NET 10 RC2"

This is fixed in the final release.

This reverts commit 5a59ac9aa8.

* fix new warnings with MSTest 4 + .NET 10

* use same Randomizer instance

* disable CA2000

* reduce CA1849 suppressions

and disable duplicate S6966

* disable preview analyzers

reverts 6c3c06d95a
2025-11-15 12:21:49 +01:00

308 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using Renci.SshNet.Common;
namespace Renci.SshNet
{
/// <summary>
/// Represents a mechanism to authenticate a given client.
/// </summary>
internal sealed class ClientAuthentication : IClientAuthentication
{
private readonly int _partialSuccessLimit;
/// <summary>
/// Initializes a new instance of the <see cref="ClientAuthentication"/> class.
/// </summary>
/// <param name="partialSuccessLimit">The number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="partialSuccessLimit"/> is less than one.</exception>
public ClientAuthentication(int partialSuccessLimit)
{
if (partialSuccessLimit < 1)
{
throw new ArgumentOutOfRangeException(nameof(partialSuccessLimit), "Cannot be less than one.");
}
_partialSuccessLimit = partialSuccessLimit;
}
/// <summary>
/// Gets the number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can
/// result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
/// </summary>
/// <value>
/// The number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can result
/// in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
/// </value>
internal int PartialSuccessLimit
{
get { return _partialSuccessLimit; }
}
/// <summary>
/// Attempts to perform authentication for a given <see cref="ISession"/> using the
/// <see cref="IConnectionInfoInternal.AuthenticationMethods"/> of the specified
/// <see cref="IConnectionInfoInternal"/>.
/// </summary>
/// <param name="connectionInfo">A <see cref="IConnectionInfoInternal"/> to use for authenticating.</param>
/// <param name="session">The <see cref="ISession"/> for which to perform authentication.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> or <paramref name="session"/> is <see langword="null"/>.</exception>
/// <exception cref="SshAuthenticationException">Failed to authenticate the client.</exception>
public void Authenticate(IConnectionInfoInternal connectionInfo, ISession session)
{
ArgumentNullException.ThrowIfNull(connectionInfo);
ArgumentNullException.ThrowIfNull(session);
session.RegisterMessage("SSH_MSG_USERAUTH_FAILURE");
session.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
session.RegisterMessage("SSH_MSG_USERAUTH_BANNER");
session.UserAuthenticationBannerReceived += connectionInfo.UserAuthenticationBannerReceived;
try
{
// the exception to report an authentication failure with
SshAuthenticationException authenticationException = null;
// try to authenticate against none
var noneAuthenticationMethod = connectionInfo.CreateNoneAuthenticationMethod();
var authenticated = noneAuthenticationMethod.Authenticate(session);
if (authenticated != AuthenticationResult.Success)
{
if (!TryAuthenticate(session, new AuthenticationState(connectionInfo.AuthenticationMethods), noneAuthenticationMethod.AllowedAuthentications, ref authenticationException))
{
throw authenticationException;
}
}
}
finally
{
session.UserAuthenticationBannerReceived -= connectionInfo.UserAuthenticationBannerReceived;
session.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE");
session.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
session.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER");
}
}
private bool TryAuthenticate(ISession session,
AuthenticationState authenticationState,
string[] allowedAuthenticationMethods,
ref SshAuthenticationException authenticationException)
{
if (allowedAuthenticationMethods.Length == 0)
{
authenticationException = new SshAuthenticationException("No authentication methods defined on SSH server.");
return false;
}
// we want to try authentication methods in the order in which they were
// passed in the ctor, not the order in which the SSH server returns
// the allowed authentication methods
var matchingAuthenticationMethods = authenticationState.GetSupportedAuthenticationMethods(allowedAuthenticationMethods);
if (matchingAuthenticationMethods.Count == 0)
{
authenticationException = new SshAuthenticationException(string.Format(CultureInfo.InvariantCulture,
"No suitable authentication method found to complete authentication ({0}).",
string.Join(',', allowedAuthenticationMethods)));
return false;
}
foreach (var authenticationMethod in authenticationState.GetActiveAuthenticationMethods(matchingAuthenticationMethods))
{
// guard against a stack overflow for servers that do not update the list of allowed authentication
// methods after a partial success
if (authenticationState.GetPartialSuccessCount(authenticationMethod) >= _partialSuccessLimit)
{
/* TODO Get list of all authentication methods that have reached the partial success limit? */
authenticationException = new SshAuthenticationException(string.Format("Reached authentication attempt limit for method ({0}).",
authenticationMethod.Name));
continue;
}
var authenticationResult = authenticationMethod.Authenticate(session);
switch (authenticationResult)
{
case AuthenticationResult.PartialSuccess:
authenticationState.RecordPartialSuccess(authenticationMethod);
if (TryAuthenticate(session, authenticationState, authenticationMethod.AllowedAuthentications, ref authenticationException))
{
authenticationResult = AuthenticationResult.Success;
}
break;
case AuthenticationResult.Failure:
authenticationState.RecordFailure(authenticationMethod);
authenticationException = new SshAuthenticationException(string.Format("Permission denied ({0}).", authenticationMethod.Name));
break;
case AuthenticationResult.Success:
authenticationException = null;
break;
default:
break;
}
if (authenticationResult == AuthenticationResult.Success)
{
return true;
}
}
return false;
}
private sealed class AuthenticationState
{
private readonly IList<IAuthenticationMethod> _supportedAuthenticationMethods;
/// <summary>
/// Records if a given <see cref="IAuthenticationMethod"/> has been tried, and how many times this resulted
/// in <see cref="AuthenticationResult.PartialSuccess"/>.
/// </summary>
/// <remarks>
/// When there's no entry for a given <see cref="IAuthenticationMethod"/>, then it was never tried.
/// </remarks>
private readonly Dictionary<IAuthenticationMethod, int> _authenticationMethodPartialSuccessRegister;
/// <summary>
/// Holds the list of authentications methods that failed.
/// </summary>
private readonly List<IAuthenticationMethod> _failedAuthenticationMethods;
public AuthenticationState(IList<IAuthenticationMethod> supportedAuthenticationMethods)
{
_supportedAuthenticationMethods = supportedAuthenticationMethods;
_failedAuthenticationMethods = new List<IAuthenticationMethod>();
_authenticationMethodPartialSuccessRegister = new Dictionary<IAuthenticationMethod, int>();
}
/// <summary>
/// Records a <see cref="AuthenticationResult.Failure"/> authentication attempt for the specified
/// <see cref="IAuthenticationMethod"/> .
/// </summary>
/// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/> for which to record the result of an authentication attempt.</param>
public void RecordFailure(IAuthenticationMethod authenticationMethod)
{
_failedAuthenticationMethods.Add(authenticationMethod);
}
/// <summary>
/// Records a <see cref="AuthenticationResult.PartialSuccess"/> authentication attempt for the specified
/// <see cref="IAuthenticationMethod"/> .
/// </summary>
/// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/> for which to record the result of an authentication attempt.</param>
public void RecordPartialSuccess(IAuthenticationMethod authenticationMethod)
{
if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out var partialSuccessCount))
{
_authenticationMethodPartialSuccessRegister[authenticationMethod] = partialSuccessCount + 1;
}
else
{
_authenticationMethodPartialSuccessRegister.Add(authenticationMethod, 1);
}
}
/// <summary>
/// Returns the number of times an authentication attempt with the specified <see cref="IAuthenticationMethod"/>
/// has resulted in <see cref="AuthenticationResult.PartialSuccess"/>.
/// </summary>
/// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/>.</param>
/// <returns>
/// The number of times an authentication attempt with the specified <see cref="IAuthenticationMethod"/>
/// has resulted in <see cref="AuthenticationResult.PartialSuccess"/>.
/// </returns>
public int GetPartialSuccessCount(IAuthenticationMethod authenticationMethod)
{
if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out var partialSuccessCount))
{
return partialSuccessCount;
}
return 0;
}
/// <summary>
/// Returns a list of supported authentication methods that match one of the specified allowed authentication
/// methods.
/// </summary>
/// <param name="allowedAuthenticationMethods">A list of allowed authentication methods.</param>
/// <returns>
/// A list of supported authentication methods that match one of the specified allowed authentication methods.
/// </returns>
/// <remarks>
/// The authentication methods are returned in the order in which they were specified in the list that was
/// used to initialize the current <see cref="AuthenticationState"/> instance.
/// </remarks>
public List<IAuthenticationMethod> GetSupportedAuthenticationMethods(string[] allowedAuthenticationMethods)
{
var result = new List<IAuthenticationMethod>();
foreach (var supportedAuthenticationMethod in _supportedAuthenticationMethods)
{
var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name;
for (var i = 0; i < allowedAuthenticationMethods.Length; i++)
{
if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod)
{
result.Add(supportedAuthenticationMethod);
break;
}
}
}
return result;
}
/// <summary>
/// Returns the authentication methods from the specified list that have not yet failed.
/// </summary>
/// <param name="matchingAuthenticationMethods">A list of authentication methods.</param>
/// <returns>
/// The authentication methods from <paramref name="matchingAuthenticationMethods"/> that have not yet failed.
/// </returns>
/// <remarks>
/// <para>
/// This method first returns the authentication methods that have not yet been executed, and only then
/// returns those for which an authentication attempt resulted in a <see cref="AuthenticationResult.PartialSuccess"/>.
/// </para>
/// <para>
/// Any <see cref="IAuthenticationMethod"/> that has failed is skipped.
/// </para>
/// </remarks>
public IEnumerable<IAuthenticationMethod> GetActiveAuthenticationMethods(List<IAuthenticationMethod> matchingAuthenticationMethods)
{
var skippedAuthenticationMethods = new List<IAuthenticationMethod>();
for (var i = 0; i < matchingAuthenticationMethods.Count; i++)
{
var authenticationMethod = matchingAuthenticationMethods[i];
// skip authentication methods that have already failed
if (_failedAuthenticationMethods.Contains(authenticationMethod))
{
continue;
}
// delay use of authentication methods that had a PartialSuccess result
if (_authenticationMethodPartialSuccessRegister.ContainsKey(authenticationMethod))
{
skippedAuthenticationMethods.Add(authenticationMethod);
continue;
}
yield return authenticationMethod;
}
foreach (var authenticationMethod in skippedAuthenticationMethods)
{
yield return authenticationMethod;
}
}
}
}
}