SSHFP DNS records
SSHFPRecord implements the SSHFP RDATA format from RFC 4255, SHA-256 preference from RFC 6594,
and the ECDSA, Ed25519, and Ed448 algorithm assignments. It operates on records that an application
has already obtained and authenticated; it does not perform DNS lookups or claim that an answer was
validated by DNSSEC.
Verify an authenticated RRset
Obtain the SSHFP RRset for the exact fully qualified hostname through a validating resolver. The resolver result must prove that the answer is DNSSEC-authenticated, or arrive over a secure channel from the validating resolver. Convert each record's three RDATA fields:
import { Client, SSHFPRecord, verifySSHFP, type SSHFPVerificationResult } from "@bunkerch/modernssh"
// Supplied only after the application has authenticated the DNSSEC answer for this exact name.
declare const authenticatedSSHFPText: readonly string[]
const records = authenticatedSSHFPText.map((value) => SSHFPRecord.parseText(value))
const client = new Client({
hostname: "ssh.example.com",
username: "deploy",
})
client.hooker.hook("hostKey", (_hook, decision, serverPublicKey) => {
const result: SSHFPVerificationResult = verifySSHFP(serverPublicKey, records)
decision.allowHostKey = result === "match"
})
await client.connect()verifySSHFP() returns:
"match"when one record using the preferred supported fingerprint type matches the key."mismatch"when applicable supported records exist but none match."no-supported-records"when the key has no supported SSHFP assignment or the RRset has no supported record for that key algorithm.
Treat both non-match results as denial unless another explicitly configured host-verification policy succeeds. An unsigned DNS answer, an authenticated denial for a different owner name, or a resolver's unverified data must never be promoted to trusted records.
When applicable SHA-256 records exist, only that complete SHA-256 set is checked. A SHA-256
mismatch returns "mismatch" even if a SHA-1 record would match. This prevents fallback contrary
to RFC 6594.
Publish fingerprints
SHA-256 is generated by default:
import { readFile } from "node:fs/promises"
import { PublicKey, SSHFPFingerprintType, SSHFPRecord } from "@bunkerch/modernssh"
const hostKey = PublicKey.parseString(await readFile("/etc/ssh/ssh_host_ed25519_key.pub", "utf8"))
const sha256 = SSHFPRecord.fromPublicKey(hostKey)
const sha1 = SSHFPRecord.fromPublicKey(hostKey, SSHFPFingerprintType.SHA1)
console.log(`ssh.example.com. IN SSHFP ${sha256}`)
console.log(`ssh.example.com. IN SSHFP ${sha1}`)toString() produces only the three presentation-format RDATA fields. parseText() accepts the
same form, for example 4 2 a87f...2401; DNS owner names, TTLs, classes, comments, parentheses, and
the SSHFP mnemonic belong to the DNS parser.
serialize() and parse() operate on complete binary SSHFP RDATA: one algorithm octet, one
fingerprint-type octet, and the opaque fingerprint. Unknown numeric assignments round-trip so DNS
software can preserve future records, but verification ignores fingerprint types it cannot
compute. Known SHA-1 and SHA-256 values require their exact digest lengths. Inputs are bounded by
the DNS RDATA size, and retained or returned fingerprint buffers are defensive copies.
Plain RSA, DSA, NIST ECDSA, Ed25519, and Ed448 public keys have SSHFP algorithm assignments. Certificate and security-key algorithm names do not, so generation rejects them rather than guessing an underlying identity.