Skip to content

Commit

Permalink
Support auth using 'none'.
Browse files Browse the repository at this point in the history
  • Loading branch information
tmds committed Dec 10, 2024
1 parent b7e68c7 commit 4b2c0b6
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 9 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class SftpFile : Stream
}
class SshClientSettings
{
static IReadOnlyList<Credential> DefaultCredentials { get; } = [ PrivateKeyCredential("~/.ssh/id_rsa"), KerberosCredential() ]
static IReadOnlyList<Credential> DefaultCredentials { get; } = [ PrivateKeyCredential("~/.ssh/id_rsa"), KerberosCredential(), NoCredential() ]
static IReadOnlyList<string> DefaultUserKnownHostsFilePaths { get; } = [ '~/.ssh/known_hosts' ]
static IReadOnlyList<string> DefaultGlobalKnownHostsFilePaths { get; } = [ '/etc/ssh/known_hosts' ]

Expand Down Expand Up @@ -491,6 +491,10 @@ class KerberosCredential : Credential
{
KerberosCredential(NetworkCredential? credential = null, bool delegateCredential = false, string? targetName = null);
}
class NoCredential : Credential
{
NoCredential();
}
// Base class.
class SshException : Exception
{ }
Expand Down Expand Up @@ -558,6 +562,7 @@ Authentications:
- publickey (`PrivateKeyCredential`)
- password (`PasswordCredential`)
- gssapi-with-mic (`KerberosCredential`)
- none (`NoCredential`)

## Design

Expand Down
10 changes: 10 additions & 0 deletions src/Tmds.Ssh/NoCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is part of Tmds.Ssh which is released under MIT.
// See file LICENSE for full license details.

namespace Tmds.Ssh;

public sealed class NoCredential : Credential
{
public NoCredential()
{ }
}
6 changes: 6 additions & 0 deletions src/Tmds.Ssh/SshClientLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ public static void PacketSend(this ILogger<SshClient> logger, ReadOnlyPacket pac
Message = "Auth {AuthMethod} method partial success, continue with: {AllowedMethods}")]
public static partial void PartialSuccessAuth(this ILogger<SshClient> logger, Name authMethod, Name[] allowedMethods);

[LoggerMessage(
EventId = 30,
Level = LogLevel.Information,
Message = "Auth using none")]
public static partial void NoneAuth(this ILogger<SshClient> logger);

struct PacketPayload // TODO: implement ISpanFormattable
{
private ReadOnlyPacket _packet;
Expand Down
2 changes: 1 addition & 1 deletion src/Tmds.Ssh/SshClientSettings.Defaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ partial class SshClientSettings
private static IReadOnlyList<Credential> CreateDefaultCredentials()
{
List<Credential> credentials = new();
credentials.Add(new KerberosCredential());
foreach (var identityFile in DefaultIdentityFiles)
{
credentials.Add(new PrivateKeyCredential(identityFile));
}
credentials.Add(new KerberosCredential());
credentials.Add(new NoCredential());
return credentials.AsReadOnly();
}

Expand Down
13 changes: 12 additions & 1 deletion src/Tmds.Ssh/SshClientSettings.SshConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ partial class SshClientSettings
AlgorithmNames.HostBased,
AlgorithmNames.PublicKey,
AlgorithmNames.KeyboardInteractive,
AlgorithmNames.Password
AlgorithmNames.Password,
AlgorithmNames.None
];

internal static async ValueTask<SshClientSettings> LoadFromConfigAsync(string? userName, string host, int? port, SshConfigSettings options, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -140,6 +141,7 @@ private static List<Credential> DetermineCredentials(SshConfig config)
{
bool addPubKeyCredentials = config.PubKeyAuthentication ?? true;
bool addGssApiCredentials = config.GssApiAuthentication ?? false;
bool addNone = true;

ReadOnlySpan<Name[]> authPreferences = [
config.PreferredAuthentications ?? Array.Empty<Name>(),
Expand Down Expand Up @@ -176,6 +178,15 @@ private static List<Credential> DetermineCredentials(SshConfig config)
addPubKeyCredentials = false;
}
}
else if (algorithm == AlgorithmNames.None)
{
if (addNone)
{
credentials.Add(new NoCredential());

addNone = false;
}
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/Tmds.Ssh/SshSession.Authentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ private async Task AuthenticateAsync(SshConnection connection, CancellationToken
authResult = await GssApiAuth.TryAuthenticate(kerberosCredential, context, ConnectionInfo, Logger, ct).ConfigureAwait(false);
}
}
else if (credential is NoCredential)
{
if (TryMethod(AlgorithmNames.None))
{
authResult = await NoneAuth.TryAuthenticate(context, ConnectionInfo, Logger, ct).ConfigureAwait(false);
}
}
else
{
throw new NotImplementedException("Unsupported credential type: " + credential.GetType().FullName);
Expand Down
5 changes: 5 additions & 0 deletions src/Tmds.Ssh/UserAuthContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ private AuthResult GetAuthResult(ReadOnlyPacket packet)
{
return null;
}
// Servers don't return when 'none' is allowed.
if (method == AlgorithmNames.None)
{
return null;
}
return Array.IndexOf(_allowedAuthentications, method) >= 0;
}

Expand Down
38 changes: 38 additions & 0 deletions src/Tmds.Ssh/UserAuthentication.None.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// This file is part of Tmds.Ssh which is released under MIT.
// See file LICENSE for full license details.

using Microsoft.Extensions.Logging;

namespace Tmds.Ssh;

partial class UserAuthentication
{
// https://datatracker.ietf.org/doc/html/rfc4252 - The "none" Authentication Request
public sealed class NoneAuth
{
public static async Task<AuthResult> TryAuthenticate(UserAuthContext context, SshConnectionInfo connectionInfo, ILogger<SshClient> logger, CancellationToken ct)
{
context.StartAuth(AlgorithmNames.None);

logger.NoneAuth();

{
using var userAuthMsg = CreateNoneRequestMessage(context.SequencePool, context.UserName);
await context.SendPacketAsync(userAuthMsg.Move(), ct).ConfigureAwait(false);
}

return await context.ReceiveAuthResultAsync(ct).ConfigureAwait(false);
}

private static Packet CreateNoneRequestMessage(SequencePool sequencePool, string userName)
{
using var packet = sequencePool.RentPacket();
var writer = packet.GetWriter();
writer.WriteMessageId(MessageId.SSH_MSG_USERAUTH_REQUEST);
writer.WriteString(userName);
writer.WriteString("ssh-connection");
writer.WriteString("none");
return packet.Move();
}
}
}
28 changes: 28 additions & 0 deletions test/Tmds.Ssh.Tests/NoneAuthenticationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Xunit;

namespace Tmds.Ssh.Tests;

[Collection(nameof(NoneAuthSshServerCollection))]
public class NoneAuthenticationTests
{
private readonly NoneAuthSshServer _sshServer;

public NoneAuthenticationTests(NoneAuthSshServer sshServer)
{
_sshServer = sshServer;
}

[Fact]
public async Task Success()
{
var settings = new SshClientSettings(_sshServer.Destination)
{
UserKnownHostsFilePaths = [ _sshServer.KnownHostsFilePath ],
Credentials = [ new NoCredential() ]
};

using var client = new SshClient(settings);

await client.ConnectAsync();
}
}
37 changes: 32 additions & 5 deletions test/Tmds.Ssh.Tests/SshServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ namespace Tmds.Ssh.Tests;

public class SshServer : IDisposable
{
private const string ContainerImageName = "test_sshd:latest";
private string? ContainerImageName { get; }
private const string ContainerBuildContext = "sshd_container";

public static bool HasKerberos = HasExecutable("kinit");

public string TestUser => "testuser";
public NetworkCredential TestKerberosCredential => new NetworkCredential($"{TestUser}@REALM.TEST", TestUserPassword);
public string TestUserHome => $"/home/{TestUser}";
public string TestUserPassword => "secret";
public string TestUserPassword { get; }
public NetworkCredential TestKerberosCredential => new NetworkCredential($"{TestUser}@REALM.TEST", "secret");
public string TestUserIdentityFile => $"{ContainerBuildContext}/user_key_rsa";
public string TestUserIdentityFileEcdsa256 => $"{ContainerBuildContext}/user_key_ecdsa_256";
public string TestUserIdentityFileEcdsa384 => $"{ContainerBuildContext}/user_key_ecdsa_384";
Expand Down Expand Up @@ -81,8 +81,11 @@ private void WriteMessage(string message)
_messageSink.OnMessage(new DiagnosticMessage(message));
}

protected SshServer(string? sshdConfig, IMessageSink messageSink)
protected SshServer(string? sshdConfig, IMessageSink messageSink, string userPassword = "secret")
{
TestUserPassword = userPassword;
ContainerImageName = $"test_{GetType().Name.ToLowerInvariant()}:latest";

_messageSink = messageSink;
WriteMessage("Starting SSH server for tests.");

Expand All @@ -91,7 +94,7 @@ protected SshServer(string? sshdConfig, IMessageSink messageSink)

try
{
Run("podman", "build", "-t", ContainerImageName, ContainerBuildContext);
Run("podman", "build", $"--build-arg=PASSWORD={TestUserPassword}", "-t", ContainerImageName, ContainerBuildContext);
IPAddress interfaceAddress = IPAddress.Loopback;
_host = interfaceAddress.ToString();
_port = PickFreePort(interfaceAddress);
Expand Down Expand Up @@ -466,3 +469,27 @@ public class MultiMethodAuthSshServerCollection : ICollectionFixture<MultiMethod
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}

public class NoneAuthSshServer : SshServer
{
public const string Config =
"""
PasswordAuthentication yes
PermitEmptyPasswords yes
AuthenticationMethods none
""";

public NoneAuthSshServer(IMessageSink messageSink) :
base(Config, messageSink,
// For OpenSSH to allow 'none', the user needs an empty password.
userPassword: "")
{ }
}

[CollectionDefinition(nameof(NoneAuthSshServerCollection))]
public class NoneAuthSshServerCollection : ICollectionFixture<NoneAuthSshServer>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
6 changes: 5 additions & 1 deletion test/Tmds.Ssh.Tests/sshd_container/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
ARG PASSWORD=secret

FROM quay.io/centos/centos:stream10-development

ARG PASSWORD

EXPOSE 88/tcp
EXPOSE 88/udp

Expand All @@ -10,7 +14,7 @@ RUN dnf install -y krb5-{libs,server,workstation} openssh-server passwd socat do
# Add a user.
RUN useradd -ms /bin/bash testuser
RUN useradd -ms /bin/bash --badname testuser@REALM.TEST
RUN echo 'secret' | passwd --stdin testuser
RUN [ -z "$PASSWORD" ] && passwd --delete testuser || echo "$PASSWORD" | passwd --stdin testuser

# Add test subsystem configuration
RUN echo 'Subsystem test_subsystem /usr/bin/sh' >> /etc/ssh/sshd_config
Expand Down

0 comments on commit 4b2c0b6

Please sign in to comment.