Help us shape the future of Go! Take the 2026 Go Developer Survey

revocation

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: BSD-3-Clause Imports: 21 Imported by: 2

README

go-authn/revocation

Distributing revocation lists, SSH KRLs and X.509 CRLs, from the authority that writes them to every server that must refuse what they list, without trusting anything in between and without ever mistaking a stale list for a current one.

  • PROTOCOL.md: what is verified, and why.
  • revokd: an agent that fetches the lists, keeps only those that verify, and writes them where sshd and TLS servers read them. When a list lapses, it fails closed for sshd. With listen, it serves the verified lists to other agents as a mirror.
  • the Go package: verifies, orders and fetches lists, and signs KRLs for an issuer (go-authn/bridge).

Why

OpenSSH has no way to distribute a KRL. Practice is configuration management, rsync or cron with curl, and two things are missing whatever the transport:

  • A KRL is not signed. OpenSSH stopped verifying the KRL signature section, and from 9.4 refuses a KRL that has one. So a KRL is only as authentic as the last hop that carried it.
  • Nothing says a KRL is stale. sshd reads a file that stopped updating months ago as the current list. TUF calls serving such a file an indefinite freeze attack.

A CRL has neither problem: it is signed, and has a nextUpdate past which OpenSSL refuses it (measured below). This brings KRLs to the same place:

KRL CRL
authentic detached SSHSIG by the SSH CA key, namespace krl@go-authn.github.io, checked with ssh-keygen -Y verify too signed by its CA
current expires@go-authn.github.io, a non-critical extension, which sshd ignores (go-authn/krl v0.2.0) nextUpdate
never backwards krl_version, then generation date CRL Number, then thisUpdate
fail closed revokd writes a KRL that revokes the CA key: every certificate of that CA is refused OpenSSL refuses an expired CRL by itself

Since every list verifies by itself, the transport can be HTTPS, HTTP inside a cluster, a CDN, a mirror, or a file. The transport decides how fast a change arrives, never whether a forged or stale list is accepted.

revokd

state_dir = "/var/lib/revokd"     # the verified copies: rollback protection, and what a mirror serves
refresh   = "1m"

source "univ-a-ssh" {
  url    = "https://idp.univ-a.fr/ssh/krl"   # and .../ssh/krl.sig
  ssh_ca = "/etc/ssh/ca/univ-a.pub"          # what the list must be signed by
}
source "univ-b-ssh" {
  url    = "http://mirror.cluster.local:8080/univ-b-ssh"   # a mirror: plain http is fine
  ssh_ca = "/etc/ssh/ca/univ-b.pub"
}
source "univ-a-x509" {
  url     = "https://idp.univ-a.fr/x509/crl"
  x509_ca = "/etc/ssl/univ-a-ca.pem"
  max_age = "2h"                             # optional, on top of nextUpdate
}

output "sshd" {                              # sshd_config: RevokedKeys /etc/ssh/revoked.krl
  path    = "/etc/ssh/revoked.krl"
  sources = ["univ-a-ssh", "univ-b-ssh"]     # merged into one file
}
output "nginx" {
  path      = "/etc/nginx/univ-a.crl"
  sources   = ["univ-a-x509"]
  format    = "pem"
  on_change = ["systemctl", "reload", "nginx"]   # retried at each sync until it succeeds
}

listen = ":8080"                             # optional: serve the verified lists (a mirror)
revokd -config /etc/revokd.hcl          # run
revokd -config /etc/revokd.hcl -once    # one sync, for cron: exit 1 when a list is not current
  • One file for sshd. RevokedKeys takes a single file before OpenSSH 10.3 (multiple files arrived with openssh-portable 135a622, 2026-02-11; Debian 13 ships 10.0, Ubuntu 24.04 9.6), so the KRL sources of an output are merged (krl.Builder.Merge). A source whose list is not current contributes its CA key, revoked, and the file's comment says FAIL CLOSED and names it (ssh-keygen -Q -l -f shows it).
  • No reload for sshd. sshd reads RevokedKeys at each authentication, so the atomic rename is enough. An unreadable file refuses every login (sshd_config(5)).
  • The mirror serves GET /<source> (with ETag, If-None-Match), GET /<source>.sig (with If-Match, 412 when the list changed in between), and GET /healthz (503 while a source has no current list). It serves from memory the pair its fetcher holds, so a list and a signature always match. Mirrors chain: a mirror's source can be another mirror.

Measured

  • A real sshd judges what revokd writes (TestSSHDJudgesWhatRevokdWrites: OpenSSH 10.3p1 on macOS, and 9.6p1 on the Ubuntu CI runner, a single-file RevokedKeys sshd). A certificate logs in with nothing revoked; once its serial is revoked it is refused; with a list revoking nothing that then lapses, it is refused (the fail-closed list); with a fresh list it logs in again. sshd is never restarted. Removing the fail-closed list from revokd turns this red.
  • OpenSSL fails closed on its own: openssl verify -crl_check with a CRL past its nextUpdate answers error 12 ... CRL has expired (OpenSSL 3.6.4).
  • ssh-keygen judges the signatures both ways: ssh-keygen -Y verify accepts what SignKRL writes, and VerifyKRL accepts what ssh-keygen -Y sign writes. Each refuses the other namespace.
  • RSA with SHA-1 (ssh-rsa), which PROTOCOL.sshsig forbids, is accepted by x/crypto's and hiddeco/sshsig's Verify. It is refused because hiddeco/sshsig's Unarmor refuses that format. A test holds the dependency to it, with rsa-sha2-512 as the control.
  • A poll costs the mirror about 0.6 µs of handler time when nothing changed (304), whatever the size of the list. The first version hashed the list per request: 122 µs at 100 000 revocations. A full download of 100 000 sparse revocations is 256 KiB and costs 22 µs to serve and 2.6 ms to verify. So 10 000 servers polling every minute make 167 requests a second, which a mirror handles with a fraction of a core, TLS aside (go test -run '^$' -bench Mirror ./cmd/revokd).

What it does not do

A revocation reaches a server no faster than the server polls (refresh), plus the issuer's delay in re-issuing. When the issuer is down, it is refused from the list's expiry onward. Whoever holds a CA's key can sign any list, as they can sign any certificate.

License

BSD 3-Clause.

Documentation

Overview

Package revocation verifies, orders and fetches revocation lists -- SSH KRLs with a detached SSHSIG signature, and X.509 CRLs -- for a distribution that trusts nothing in between: every list is checked against the CA it revokes for, refused once it is no longer current, and never let replace a newer one.

VerifyKRL and VerifyCRL check a list; List.Current and List.Follows say whether it may be used now and whether it may replace the one held; Fetcher fetches one source with all three; SignKRL signs a KRL for an issuer; FailClosedKRL is what a distributor writes for sshd when it has no current list.

PROTOCOL.md says what is verified and why; the revokd command distributes lists with this package.

Index

Constants

View Source
const Namespace = "krl@go-authn.github.io"

Namespace is the SSHSIG namespace a KRL's signature is made in (PROTOCOL.sshsig: it keeps a signature from being taken for another purpose). ssh-keygen -Y verify -n krl@go-authn.github.io checks it.

Variables

View Source
var (
	// ErrExpired is a list past its expiry, or older than the caller's
	// max_age: not current.
	ErrExpired = errors.New("revocation: the list is no longer current")
	// ErrRollback is a list older than the one held.
	ErrRollback = errors.New("revocation: the list is older than the one held")
)

Errors a caller tells apart.

Functions

func FailClosedKRL

func FailClosedKRL(ca ssh.PublicKey, now time.Time) ([]byte, error)

FailClosedKRL is the list a distributor writes for sshd when it has no current list from ca: ca's own key revoked, which sshd checks for every certificate ca signed (krl.c ssh_krl_check_key) -- so every one of them is refused, and nothing else. It is not signed: it is written locally, for the reader on this machine, never distributed.

func FilePath

func FilePath(u string) (string, error)

FilePath is the local path a file:// URL names: file:///etc/x is /etc/x, and on Windows file:///C:/x is C:\x (RFC 8089, appendix E.2).

func FileURL

func FileURL(path string) string

FileURL is the file:// URL of a local path, the inverse of FilePath.

func SignKRL

func SignKRL(raw []byte, ca ssh.Signer) ([]byte, error)

SignKRL signs a KRL for distribution: an armored SSHSIG signature by the CA key, in Namespace, hashed with SHA-512 (ssh-keygen -Y sign's default). The KRL must say when it expires (krl.Builder.SetExpires): a verifier refuses one that does not.

Types

type Fetcher

type Fetcher struct {
	// contains filtered or unexported fields
}

Fetcher fetches one source, keeping the last list that verified.

func NewFetcher

func NewFetcher(src Source, held *List) (*Fetcher, error)

NewFetcher returns a Fetcher for src that holds held, the copy kept from before (the file last written, read back): a fetched list must follow it.

func (*Fetcher) Fetch

func (f *Fetcher) Fetch(ctx context.Context) (*List, bool, error)

Fetch fetches the source once. It returns the list held afterwards, and whether it changed. A list that does not verify, is not current, or does not follow the one held is refused with an error, and the one held is kept: never the empty answer, which would revoke nothing.

func (*Fetcher) Held

func (f *Fetcher) Held() *List

Held is the last list that verified, or nil.

type Kind

type Kind int

Kind is what a list is.

const (
	// KRL is an OpenSSH key revocation list with its detached SSHSIG
	// signature.
	KRL Kind = iota + 1
	// CRL is an X.509 certificate revocation list, DER-encoded.
	CRL
)

func (Kind) String

func (k Kind) String() string

type List

type List struct {
	Kind Kind
	// Raw is the list as it was signed: what sshd, OpenSSL and every other
	// reader take.
	Raw []byte
	// Sig is a KRL's armored SSHSIG signature; nil for a CRL, which is
	// signed inside.
	Sig []byte
	// Version is the KRL's krl_version or the CRL Number: the order lists
	// of one issuer come in.
	Version *big.Int
	// Issued is the KRL's generation date or the CRL's thisUpdate.
	Issued time.Time
	// Expires is the KRL's expires@go-authn.github.io extension or the
	// CRL's nextUpdate: when the list stops being current.
	Expires time.Time

	// KRL is the parsed KRL, for a KRL.
	KRL *krl.KRL
	// CRL is the parsed CRL, for a CRL.
	CRL *x509.RevocationList
}

List is a revocation list that verified: its signature by the CA it was checked against, and its form. Whether it is still current is Current's question; whether it may replace another is Follows'.

func VerifyCRL

func VerifyCRL(raw []byte, ca *x509.Certificate) (*List, error)

VerifyCRL checks a CRL, DER or PEM, against the CA certificate ca. It refuses: a signature by anything else, or an issuer that is not ca's subject; a CRL with no number or no nextUpdate, which could be neither ordered nor aged; a delta CRL, which only means something with its base (RFC 5280 5.2.4); and a critical extension it does not understand, nor any critical entry extension.

func VerifyKRL

func VerifyKRL(raw, sig []byte, ca ssh.PublicKey) (*List, error)

VerifyKRL checks a KRL and its armored SSHSIG signature against the SSH CA key ca. It refuses: a signature by any other key, in another namespace, or in the RSA SHA-1 form PROTOCOL.sshsig forbids (hiddeco/sshsig refuses it when it reads the signature; x/crypto alone would verify it); a KRL sshd would not load (go-authn/krl refuses what OpenSSH refuses, and OpenSSH from 9.4 refuses a KRL with a signature section); and a KRL that does not say when it expires, which a reader could never tell from a stale one.

func (*List) Current

func (l *List) Current(now time.Time, maxAge time.Duration) error

Current reports whether l may be used at now: not past its expiry, and, when maxAge is not zero, issued no more than maxAge before now. A list issued in the future -- an issuer's clock ahead by more than skew -- is not current either: its age cannot be known.

func (*List) Follows

func (l *List) Follows(held *List) error

Follows reports whether l may replace held, a list of the same issuer: a higher version, or the same version issued later (the same revocations, re-issued with a new expiry). Anything else is a rollback -- a replayed or restored older list, which would un-revoke what the newer one revoked. A nil held is followed by anything.

func (*List) Same

func (l *List) Same(o *List) bool

Same reports whether l and o are the same issue of a list.

type Source

type Source struct {
	// URL is http://, https:// or file://. The list verifies by itself, so
	// the transport decides how fast a change arrives, never whether a
	// forged or stale list is taken. A KRL's signature is at URL+".sig".
	URL  string
	Kind Kind
	// SSHCA is the CA key a KRL must be signed by; X509CA the certificate a
	// CRL must be signed by.
	SSHCA  ssh.PublicKey
	X509CA *x509.Certificate
	// MaxAge, when not zero, bounds a list's age from its issue, besides
	// its own expiry.
	MaxAge time.Duration
	// Client fetches http and https URLs; nil is a client with a one
	// minute timeout.
	Client *http.Client
	// Clock is the time a list's currency is judged at; nil is time.Now.
	// One clock for the fetcher and its caller, or they disagree on what
	// is current.
	Clock func() time.Time
}

Source is where a list comes from and what it must verify against.

Directories

Path Synopsis
cmd
revokd command
Command revokd distributes revocation lists: it fetches SSH KRLs and X.509 CRLs, keeps only those that verify (signed by the CA configured for them, current, never older than the copy held), writes them where sshd and TLS servers read them -- failing closed for sshd when a list lapses -- and, with listen, serves the verified copies to other revokd as a mirror.
Command revokd distributes revocation lists: it fetches SSH KRLs and X.509 CRLs, keeps only those that verify (signed by the CA configured for them, current, never older than the copy held), writes them where sshd and TLS servers read them -- failing closed for sshd when a list lapses -- and, with listen, serves the verified copies to other revokd as a mirror.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL