modernssh

Key revocation lists

KeyRevocationList loads the binary KRL format produced by ssh-keygen. It can reject plain public keys, certificates, a certificate's embedded key, or every certificate signed by a revoked authority.

import { KeyRevocationList, PublicKey } from "@bunkerch/modernssh"

const revocations = await KeyRevocationList.load("/etc/ssh/revoked_keys")
const key = PublicKey.parse(serializedKey)

if (revocations.isRevoked(key)) {
    throw new Error("SSH key is revoked")
}

isRevoked() accepts either a PublicKey or its complete SSH wire serialization. Parsing and checks are synchronous after load() resolves, so the same instance can be used from client host verification and awaited server authentication hooks.

Host verification

A KRL answers only whether a key is revoked; it does not establish which key belongs to a host. Combine it with KnownHosts so both policies must pass:

import { Client, KeyRevocationList, KnownHosts, KnownHostsError } from "@bunkerch/modernssh"

const hostname = "ssh.example.com"
const knownHosts = await KnownHosts.load("/etc/ssh/ssh_known_hosts")
const revocations = await KeyRevocationList.load("/etc/ssh/revoked_keys")

const client = new Client({
    hostname,
    username: "deploy",
})
client.hooker.hook("hostKey", (hook, decision, key) => {
    if (revocations.isRevoked(key)) {
        decision.rejection = new Error("SSH host key is revoked")
        hook.stopPropagation()
        return
    }
    const trust = knownHosts.check(hostname, key)
    if (trust.status === "trusted") decision.allowHostKey = true
    else {
        decision.rejection = new KnownHostsError(trust, hostname)
        hook.stopPropagation()
    }
})

await client.connect()

Load both files before constructing the client. An async EventEmitter listener is not a safe place to perform host trust decisions because EventEmitter does not await returned promises.

Supported records

The parser supports explicit keys, SHA-1 and SHA-256 fingerprints, certificate serial lists, serial ranges, compact serial bitmaps, certificate key identifiers, authority-specific and all-authority certificate sections, optional extensions, and consecutive signature sections at the end of a KRL. Certificate checks also cover the plain embedded key and signing authority, matching ssh-keygen -Q behavior.

Unknown critical extensions, unknown sections, nonzero format flags, serial zero, wrapped serial bitmaps, malformed keys, invalid lengths, non-canonical integers, NUL text, and trailing data fail during parsing. Optional unknown extensions are ignored. Input is copied before it is retained, and files larger than 16 MiB are rejected.

Every embedded KRL signature is verified over its exact required prefix while parsing. Invalid signatures, a non-signature section after the first signature, or a malformed signing key fail the complete KRL. Cryptographic validity does not establish trust: an attacker can create a different KRL and sign it with their own key. When loading from an untrusted location, require an exact trusted signer explicitly:

import { KeyRevocationList, PublicKey } from "@bunkerch/modernssh"

const revocations = await KeyRevocationList.load(downloadedPath)
const trustedSigner = PublicKey.parseString(configuredSigningKey)

if (!revocations.isSignedBy(trustedSigner)) {
    throw new Error("KRL is not signed by the configured authority")
}

isSignedBy() returns true only for a key whose embedded signature was already verified. Requiring it also detects an attacker who strips every signature. The published format notes that current OpenSSH versions refuse signature sections and recommends separately authenticated SSHSIG files, which can be created and checked with SSHSignature as described in Detached SSH signatures. Signed KRLs may therefore not be consumable by system tools. Unsigned KRLs distributed through an already authenticated and integrity-protected channel remain supported.

Rollback policy

The header metadata is available as version, generatedAt, and comment. version and generatedAt are bigint Unix values, preserving the complete unsigned 64-bit fields.

The format does not itself prevent an attacker from replacing a KRL with an older valid file. Applications that update revocation policy should persist the highest accepted version and reject a lower value before replacing the active instance. The library does not invent a rollback policy because storage and deployment authority belong to the application.

On this page