Public Access
Refuse a pinned host that answers with a key of another type, rather than asking whether to trust it
Pins are stored per algorithm, so an endpoint pinned for ssh-ed25519 that was answered with only an ECDSA or RSA key found no pin and raised SshHostKeyUnknownException: the first-use question, where a pin exists to produce a hard refusal. Anybody in the middle needed nothing more than a key of a different type. The connection factory now reads every pin for the endpoint before the handshake, offers the pinned algorithms first so a server that still holds its pinned key negotiates it, and in the gate accepts a key under an unpinned algorithm only when its fingerprint matches one of the endpoint's pins (one RSA key signs as rsa-sha2-256 and rsa-sha2-512). Anything else is a mismatch. IKnownHostStore gains FindForEndpointAsync for this.
This commit is contained in:
1 parent
412c314aec
commit
4c7f47c026
4 files changed
+193
-2
No files matched your search
@@ -0,0 +1,69 @@
|
||||
namespace DodoSSH.Client.Ssh.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// A pin is a pin for the endpoint, whichever key type the server answers with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[Collection(SshCollection.Name)]
|
||||
public sealed class HostKeyPinningTests(SshServerFixture fixture)
|
||||
{
|
||||
private static CancellationToken Token => TestContext.Current.CancellationToken;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<SshHostKeyMismatchException>(async () =>
|
||||
await new SshNetConnectionFactory(knownHosts).ConnectAsync(Request(), progress: null, Token));
|
||||
|
||||
mismatch.PinnedFingerprint.ShouldBe("SHA256:the-key-that-was-approved");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The same public key under another algorithm's name — an RSA key signs as <c>rsa-sha2-256</c> or
|
||||
/// <c>rsa-sha2-512</c> — is the key that was approved, and its fingerprint is what says so.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TheApprovedKeyUnderAnotherAlgorithmName_IsStillTrusted()
|
||||
{
|
||||
var learned = new InMemoryKnownHostStore();
|
||||
|
||||
var unknown = await Should.ThrowAsync<SshHostKeyUnknownException>(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<SshHostKeyUnknownException>(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));
|
||||
}
|
||||
Reference in new issue
Block a user