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)); }