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)); } }