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

@@ -145,6 +145,26 @@ public sealed class VaultKnownHostStore : IKnownHostStore
}
}
/// <inheritdoc />
/// <remarks>From the snapshot, like <see cref="FindAsync"/>, and for the same reason.</remarks>
public ValueTask<IReadOnlyList<HostKeyPresentation>> FindForEndpointAsync(
string host,
int port,
CancellationToken cancellationToken)
{
lock (gate)
{
IReadOnlyList<HostKeyPresentation> found = pins.Values
.Where(pin =>
KnownHostIdentity.Comparer.Equals(pin.Secret.Host, host) && pin.Secret.Port == port)
.Select(pin => new HostKeyPresentation(
pin.Secret.Host, pin.Secret.Port, pin.Secret.Algorithm, pin.Secret.Fingerprint))
.ToArray();
return ValueTask.FromResult(found);
}
}
/// <inheritdoc />
/// <exception cref="InvalidOperationException">
/// The vault is not open, so there is nowhere to record trust.