using System.Security.Cryptography;
namespace DodoSSH.Client.Ssh;
///
/// OpenSSH-style SSH host key fingerprints. See docs/crypto.md §8.
///
///
///
/// A different thing from a DodoSSH identity fingerprint, and deliberately kept in the format
/// users already recognise: SHA256: followed by unpadded standard base64, exactly what
/// ssh-keygen -lf prints. A user comparing our string against the one their server
/// operator sent them must not have to wonder whether the encodings match.
///
///
/// Computed from the raw host key blob. SSH.NET exposes an MD5 FingerPrint property;
/// that is not used — MD5 is banned repo-wide, and no modern operator publishes an MD5 host
/// key fingerprint to compare against.
///
///
public static class SshHostKeyFingerprint
{
/// Prefix identifying the hash algorithm, as OpenSSH writes it.
public const string Prefix = "SHA256:";
///
/// Formats a host key blob as SHA256:<base64>.
///
///
/// The raw key blob as it arrived on the wire — the same bytes OpenSSH hashes, not a parsed
/// or re-encoded form.
///
public static string Format(ReadOnlySpan hostKeyBlob)
{
if (hostKeyBlob.IsEmpty)
{
throw new ArgumentException("A host key blob is required.", nameof(hostKeyBlob));
}
Span digest = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(hostKeyBlob, digest);
// Standard base64, not base64url, and unpadded — OpenSSH's exact rendering. Using the
// URL alphabet here would produce a string that looks right and never matches.
return Prefix + Convert.ToBase64String(digest).TrimEnd('=');
}
///
/// Compares two fingerprints in constant time.
///
///
/// A fingerprint is not a secret, so this is not about a timing oracle. It is about refusing
/// to let a mismatch be decided by ordinal string comparison somewhere that later gets
/// "optimised" into a case-insensitive or culture-aware comparison — base64 is
/// case-sensitive, and a case-insensitive match here would accept a different key.
///
public static bool Equal(string? left, string? right)
{
if (left is null || right is null || left.Length != right.Length)
{
return false;
}
return CryptographicOperations.FixedTimeEquals(
System.Text.Encoding.UTF8.GetBytes(left),
System.Text.Encoding.UTF8.GetBytes(right));
}
}