namespace DodoSSH.Client.Ssh.Tests;
///
/// A pin is a pin for the endpoint, whichever key type the server answers with.
///
///
/// Pins are stored per algorithm, and the gap these close was that an endpoint whose only pins were for
/// other algorithms read as a host never seen before. Somebody in the middle needed nothing more than a key
/// of a different type to turn the hard refusal a pin exists for into a first-use question the user is
/// invited to click through.
///
[Collection(SshCollection.Name)]
public sealed class HostKeyPinningTests(SshServerFixture fixture)
{
private static CancellationToken Token => TestContext.Current.CancellationToken;
///
/// The endpoint is pinned under a key type this server does not have, which is exactly what a server in
/// the middle looks like from here: the pinned key is not on offer, and a key of another type is.
///
[Fact]
public async Task AKeyOfAnotherTypeOnAPinnedEndpoint_IsAMismatchRatherThanAnUnknownHost()
{
var knownHosts = new InMemoryKnownHostStore();
await knownHosts.TrustAsync(
new HostKeyPresentation(fixture.Host, fixture.Port, "ssh-dss", "SHA256:the-key-that-was-approved"),
Token);
var mismatch = await Should.ThrowAsync(async () =>
await new SshNetConnectionFactory(knownHosts).ConnectAsync(Request(), progress: null, Token));
mismatch.PinnedFingerprint.ShouldBe("SHA256:the-key-that-was-approved");
}
///
/// The same public key under another algorithm's name — an RSA key signs as rsa-sha2-256 or
/// rsa-sha2-512 — is the key that was approved, and its fingerprint is what says so.
///
[Fact]
public async Task TheApprovedKeyUnderAnotherAlgorithmName_IsStillTrusted()
{
var learned = new InMemoryKnownHostStore();
var unknown = await Should.ThrowAsync(async () =>
await new SshNetConnectionFactory(learned).ConnectAsync(Request(), progress: null, Token));
var knownHosts = new InMemoryKnownHostStore();
await knownHosts.TrustAsync(unknown.Presentation with { Algorithm = "x-renamed-algorithm" }, Token);
await using var connection = await new SshNetConnectionFactory(knownHosts)
.ConnectAsync(Request(), progress: null, Token);
connection.ShouldNotBeNull();
}
[Fact]
public async Task AnEndpointWithNoPinsAtAll_IsStillAnUnknownHost() =>
await Should.ThrowAsync(async () =>
await new SshNetConnectionFactory(new InMemoryKnownHostStore())
.ConnectAsync(Request(), progress: null, Token));
private SshConnectionRequest Request() => new(
fixture.Host,
fixture.Port,
SshServerFixture.Username,
new SshPasswordCredential(SshServerFixture.Password));
}