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:
jaap-jan committed 2026-10-02 13:16:30 +02:00
1 parent 412c314aec
commit 4c7f47c026
4 files changed
+193 -2

No files matched your search

+32
View File
@@ -64,6 +64,22 @@ public interface IKnownHostStore
/// </remarks>
ValueTask<string?> FindAsync(string host, int port, string algorithm, CancellationToken cancellationToken);
/// <summary>Returns every key pinned for one endpoint, whatever the algorithm.</summary>
/// <remarks>
/// <para>
/// ◆ <b>What stops a pin being sidestepped by changing algorithm.</b> <see cref="FindAsync"/> answers
/// per algorithm, so on its own a host pinned for <c>ssh-ed25519</c> that is answered by somebody offering
/// only <c>ecdsa-sha2-nistp256</c> looks like a host never seen before — and "never seen before" is a
/// question the user is asked, where "changed" is a refusal. Knowing the endpoint has pins at all is what
/// lets the connection factory tell the two apart; see <c>SshNetConnectionFactory</c>.
/// </para>
/// <para>The same rule as <see cref="FindAsync"/>: no I/O.</para>
/// </remarks>
ValueTask<IReadOnlyList<HostKeyPresentation>> FindForEndpointAsync(
string host,
int port,
CancellationToken cancellationToken);
/// <summary>Records a host key as trusted.</summary>
ValueTask TrustAsync(HostKeyPresentation presentation, CancellationToken cancellationToken);
@@ -156,6 +172,22 @@ public sealed class InMemoryKnownHostStore : IKnownHostStore
}
}
/// <inheritdoc />
public ValueTask<IReadOnlyList<HostKeyPresentation>> FindForEndpointAsync(
string host,
int port,
CancellationToken cancellationToken)
{
lock (gate)
{
IReadOnlyList<HostKeyPresentation> found = pins.Values
.Where(pin => KnownHostIdentity.Comparer.Equals(pin.Host, host) && pin.Port == port)
.ToArray();
return ValueTask.FromResult(found);
}
}
/// <inheritdoc />
public ValueTask TrustAsync(HostKeyPresentation presentation, CancellationToken cancellationToken)
{