How a list is verified
A list that does not verify is never written, served or used. The last good copy stays. A fetch that fails, for any reason, never replaces it with an empty answer, which would revoke nothing.
The signature
A KRL is fetched with its signature, <url>.sig: an armored SSHSIG over
the whole file, by the SSH CA key configured as ssh_ca, in the namespace
krl@go-authn.github.io. The namespace keeps the signature from being taken
for anything else. A signer hashes with SHA-512; SHA-256 is accepted too. An RSA
signature in the SHA-1 form, which PROTOCOL.sshsig forbids, is refused.
The KRL itself must parse as sshd parses it, and must not carry OpenSSH’s own
signature section, which OpenSSH never verifies.
A list signed by any other key is refused:
$ authn-revokd -config revokd-other.hcl -once
bridge-ssh: revocation: the KRL's signature does not verify by SHA256:xm8VDcZUh44N+RQSiPbroPInzkMhN/KCWI/TC+9HPNQ: public key does not match
bridge-x509: version 1791712224691, issued 2026-10-11T09:50:24Z, expires 2026-10-11T10:50:24Z
output sshd: wrote /tmp/authn/revokd/out/other.krl
output tls: wrote /tmp/authn/revokd/out/other.crl
authn-revokd: not current: bridge-sshHere ssh_ca named a key that did not sign bridge’s list, and the error names
that key. A signature that does not verify may also be a list issued again
between the two requests, so both are fetched once more before it is reported.
This was the first fetch of that source, so there was no good copy to keep, and
the output is the fail-closed list at
once. (Before v0.6.2 the same failure carried “the list changed between it and
its signature” in front of the message above, as if the list had been issued
again.)
The signature is fetched with If-Match: <the list's ETag>. A list issued
again between the two requests answers 412, and both are fetched again.
A CRL is checked against the CA certificate configured as x509_ca: its
signature first, on the raw bytes, before the rest is parsed, so a large
CRL signed by anybody costs a signature check rather than a parse. Then its
issuer must be that certificate’s subject; it must have a CRL Number and a
nextUpdate after its thisUpdate; and it must not be a delta CRL, nor carry a
critical extension other than the CRL Number and the Authority Key Identifier,
nor a critical entry extension.
A KRL must also say when it expires, in the expires@go-authn.github.io
extension: one that does not could never be told from a stale one.
Bodies are bounded: a KRL at 32 MiB, its signature at 64 KiB, a CRL at 64 MiB.
The dates
A list is current when:
- now is before its expiry (the KRL’s extension, the CRL’s
nextUpdate); - it was not issued more than five minutes in the future: an issuer whose clock is ahead by more than that writes lists whose age cannot be known;
- with
max_age, it was issued no longer ago than that.
A list that is not current is refused when fetched. A 304 Not Modified is no
news: the copy held may have expired since, and it is judged again at every
sync.
Never older than the list held
A list’s position is its version (the KRL’s krl_version, the CRL
Number), then its issue time (the KRL’s generation date, the CRL’s
thisUpdate). A list may replace the one held only when it has a higher
version, or the same version issued later: the same revocations, issued again
with a new expiry. Anything else is a rollback: a replayed or restored older
list, still signed and unexpired, that would un-revoke what the newer one
revoked.
Across restarts too
The list held is kept in state_dir, one file per source holding the list and
its signature together, written by a single rename with the directory synced.
At start each copy is verified again (expired or not, it orders what comes
next), and a copy that no longer verifies is not trusted, not even to order the
next one, and is said so in the log.
Here a source is pointed, after a restart, at a list an earlier bridge process served nineteen seconds before, still signed and still unexpired:
$ authn-revokd -config revokd.hcl -once
bridge-ssh: version 0, issued 2026-10-11T09:50:24Z, expires 2026-10-11T10:50:24Z
bridge-x509: version 1791712224691, issued 2026-10-11T09:50:24Z, expires 2026-10-11T10:50:24Z
$ authn-revokd -config revokd-old.hcl -once
bridge-ssh: file:///tmp/authn/revokd/old/bridge.krl: revocation: the list is older than the one held: version 0 issued at 2026-10-11T09:50:05Z, the copy held at 2026-10-11T09:50:24Z
authn-revokd: not fetched: bridge-ssh (the list held is current until 2026-10-11T10:50:24Z)
$ ssh-keygen -Q -l -f out/sshd.krl
# KRL version 0
# Generated at 20261011T115024
# Comment: authn-revokd sshdThe first run wrote no output: the files already held those lists. The older
list is refused and the output keeps the newer one. The process that
refused it had never seen the newer list: it read it back from state_dir.
Since the state is what makes a rollback detectable, the directory is held to sshd’s StrictModes rule (the configuration). A crash between two writes cannot leave a pair that fails to verify, which would take the rollback guard with it: the list and its signature are one file.
What -once reports
-once exits 1 when a source has no current list, when its last fetch
failed, or when an output could not be written, and its last line says which
of the first two it is:
| last line | the source | its outputs |
|---|---|---|
not current: <source> | holds no current list: never fetched, expired, or past max_age | failed closed for sshd |
not fetched: <source> (the list held is current until <T>) | its fetch failed, but the copy held is still current | written from that copy, unchanged, until T |
Here the issuer is stopped while both lists held are still current:
$ authn-revokd -config revokd.hcl -once
bridge-ssh: revocation: Get "http://127.0.0.1:28473/ssh/krl": dial tcp 127.0.0.1:28473: connect: connection refused
bridge-x509: revocation: Get "http://127.0.0.1:28473/x509/crl": dial tcp 127.0.0.1:28473: connect: connection refused
authn-revokd: not fetched: bridge-ssh (the list held is current until 2026-10-11T10:50:24Z), bridge-x509 (the list held is current until 2026-10-11T10:50:24Z)That run left sshd.krl as it was. Once a list held lapses, the same source is
reported not current, and both kinds can share the line, as on
when a list lapses. Before v0.6.2
both cases were reported not current, which said a list had lapsed when only
its fetch had failed; the exit status was 1 then too.
The mirror’s /healthz judges by the copy held instead, and answered ok while
the issuer was down and the copies still current.
Created on by tannevaled
Modified on by tannevaled