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
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user