using System.Text;
using Renci.SshNet;
namespace DodoSSH.Client.Ssh.Tests;
///
/// Whether a key this client generates is a key anything else recognises.
///
///
///
/// The container is hand-written — see for why there was no alternative —
/// so the only question that matters is whether a real parser accepts it. SSH.NET's
/// PrivateKeyFile is the parser this application actually hands the key to, which makes it the right
/// oracle: a file that satisfies a test of my own encoding and then fails there would be a test agreeing
/// with the bug.
///
///
/// The stronger proof is in KeyAuthenticationTests, which installs a generated public line on a real
/// sshd in a container and connects with it. This class is what fails first and reads clearly when the
/// encoding is wrong.
///
///
public sealed class SshKeyGeneratorTests
{
[Theory]
[InlineData(SshKeyAlgorithm.Ed25519)]
[InlineData(SshKeyAlgorithm.Rsa4096)]
public void AGeneratedKey_IsOneSshNetCanLoad(SshKeyAlgorithm algorithm)
{
var generated = SshKeyGenerator.Generate(algorithm, "dodossh@test");
using var armour = new MemoryStream(Encoding.ASCII.GetBytes(generated.PrivateKeyArmour));
// No passphrase, which is this writer's whole stated limitation. A throw here is the encoding being
// wrong, not the key.
var parsed = new PrivateKeyFile(armour);
parsed.HostKeyAlgorithms.ShouldNotBeEmpty();
}
///
/// SshKeySecret.TryValidate refuses anything that does not begin -----BEGIN, and refuses
/// anything beginning ssh- outright because that is a pasted .pub file — the mistake
/// people actually make. Asserting it here in those terms rather than by constructing the secret: this
/// project deliberately has no reference to DodoSSH.Client.Domain, which is what keeps the SSH
/// layer testable without a cache or a keychain, and one convenience is not worth spending it.
///
[Theory]
[InlineData(SshKeyAlgorithm.Ed25519)]
[InlineData(SshKeyAlgorithm.Rsa4096)]
public void AGeneratedKey_IsOneTheKeychainWillStore(SshKeyAlgorithm algorithm)
{
var generated = SshKeyGenerator.Generate(algorithm, "dodossh@test");
generated.PrivateKeyArmour.TrimStart().ShouldStartWith("-----BEGIN");
generated.PrivateKeyArmour.TrimStart().ShouldNotStartWith("ssh-");
generated.PublicKeyLine.ShouldStartWith("ssh-", Case.Sensitive);
}
[Fact]
public void TheEd25519PublicLine_IsWhatOpenSshWrites()
{
var generated = SshKeyGenerator.Generate(SshKeyAlgorithm.Ed25519, "dodossh@test");
var parts = generated.PublicKeyLine.Split(' ');
parts.Length.ShouldBe(3);
parts[0].ShouldBe("ssh-ed25519");
parts[2].ShouldBe("dodossh@test");
// 4-byte length + "ssh-ed25519" + 4-byte length + 32-byte point.
Convert.FromBase64String(parts[1]).Length.ShouldBe(4 + 11 + 4 + 32);
}
[Fact]
public void TheFingerprint_IsTheOneSshKeygenWouldPrint()
{
var generated = SshKeyGenerator.Generate(SshKeyAlgorithm.Ed25519, "dodossh@test");
generated.Fingerprint.ShouldStartWith("SHA256:");
generated.Fingerprint.ShouldNotEndWith("=", Case.Sensitive);
// Taken over the same blob the public line carries, which is the whole definition of an OpenSSH
// fingerprint. Computing it over anything else — the armour, the file, the raw point — produces a
// string that looks right and never matches what a server reports.
var blob = Convert.FromBase64String(generated.PublicKeyLine.Split(' ')[1]);
generated.Fingerprint.ShouldBe(SshHostKeyFingerprint.Format(blob));
}
///
/// The classic openssh-key-v1 bug, and the reason it is worth twenty cases rather than one: the
/// private section is padded to a multiple of eight, and the comment is the last thing in it. So whether
/// the padding is right depends on how long the comment happens to be — a name that lands on a boundary
/// produces a file every parser loads, and one character more produces one that none of them do.
///
[Fact]
public void TheContainerIsPaddedCorrectlyForACommentOfAnyLength()
{
for (var length = 0; length < 20; length++)
{
var comment = new string('c', length);
var generated = SshKeyGenerator.Generate(SshKeyAlgorithm.Ed25519, comment);
using var armour = new MemoryStream(Encoding.ASCII.GetBytes(generated.PrivateKeyArmour));
Should.NotThrow(
() => new PrivateKeyFile(armour),
$"a comment of {length} characters must not change whether the key parses");
}
}
[Fact]
public void TwoGeneratedKeys_AreDifferent()
{
var first = SshKeyGenerator.Generate(SshKeyAlgorithm.Ed25519, "dodossh@test");
var second = SshKeyGenerator.Generate(SshKeyAlgorithm.Ed25519, "dodossh@test");
string.Equals(first.Fingerprint, second.Fingerprint, StringComparison.Ordinal)
.ShouldBeFalse("two generated keys must not share a fingerprint");
string.Equals(first.PrivateKeyArmour, second.PrivateKeyArmour, StringComparison.Ordinal)
.ShouldBeFalse("nor any of their material");
}
}