krl

package module
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: BSD-3-Clause Imports: 12 Imported by: 0

README

krl

Go Reference License CI

OpenSSH key revocation lists (KRLs) in Go: parse, build and check the binary format of OpenSSH's PROTOCOL.krl, the one sshd loads through RevokedKeys and ssh-keygen -k writes. golang.org/x/crypto/ssh has no support for them. Pure Go, CGO_ENABLED=0.

// A server refusing revoked certificates.
k, err := krl.Parse(data)
if err != nil { ... }
if k.IsRevoked(cert) { /* refuse */ }

// An issuer publishing what it revoked.
b := krl.NewBuilder(version, "revoked certificates")
b.RevokeSerial(caKey, 42)
b.RevokeSerialRange(caKey, 100, 199)
b.RevokeKeyID(nil, "alice") // nil: under any CA
b.RevokeKey(compromisedKey)
data, err := b.Marshal(time.Now())

IsRevoked follows ssh_krl_check_key in OpenSSH's krl.c: a certificate is revoked when its serial or key ID is revoked under the CA that signed it or under any CA, when its CA key is revoked, or when its own key is revoked by blob or by SHA-1 / SHA-256 fingerprint.

Judged by ssh-keygen

The tests do not let the package agree with itself. On the linux and macOS CI lanes, ssh-keygen is the oracle (a missing one fails those lanes):

test what ssh-keygen judges
TestOracleSshKeygenKRL a KRL written by ssh-keygen -k with serials (list, range, bitmap, up to 2^64-1), key IDs, three CAs plus "any CA" (-s none), explicit keys and a revoked CA key: IsRevoked answers like ssh-keygen -Q for 250 certificates (made by ssh-keygen -s) and keys. Builder given the same revocations writes the same bytes.
TestOracleFingerprints sha1:, sha256: and hash: revocations, for keys and their certificates
TestOracleRandomSerials 40 random serial sets written by both: byte-identical, same ssh-keygen -Q -l listing, and the same answers for ~9000 certificates on both sides of every edge
TestOracleMutations every truncation and ~1400 single-byte corruptions of a KRL: Parse refuses exactly what ssh-keygen refuses, and answers like it for the rest
TestOracleSigned a KRL with a signature section: read, signature skipped
TestOracleBitmapOverflow see below
TestOracleExpiresIsLoadedBySshKeygen a KRL carrying the expiry extension (below) loads and revokes as before; the control, the same section marked critical, is refused
TestOracleMergeIsTheUnion three lists written by ssh-keygen -k (every serial encoding, key IDs, "any CA", explicit keys, SHA1 and SHA256 fingerprints), merged by Builder.Merge: the merged list revokes exactly what one of them revokes, for ~180 certificates and keys
TestOracleRevokingTheCARevokesEveryCertificate a KRL naming the CA's own key revokes every certificate it signed, and nothing else

Windows runs the pure-Go tests only. The 100% coverage gate is measured on the Linux lane, where ssh-keygen is there: without it the pure-Go tests leave part of Builder.Merge and Builder.MergeCA to the oracle tests. FuzzParse (seeded with KRLs written by ssh-keygen, in testdata/) and FuzzBuilder run on CI too.

One deliberate difference

For many sparse serials close together (1, 3, 5, … 40001), ssh-keygen -k writes a bitmap wider than its own reader accepts and then refuses its own file: Invalid KRL file: bignum is too large (OpenSSH 10.3p1). Parse refuses that file too, as sshd would. Builder starts a new section before a bitmap gets that wide, so what it writes is always readable.

An expiry: expires@go-authn.github.io

A KRL has a generation date and a version, but nothing says when it stops being current, so a stale copy is indistinguishable from a fresh one: what TUF calls a freeze attack, and what a CRL's nextUpdate (RFC 5280, 5.1.2.5) prevents. Builder.SetExpires writes that time in an extension section, named name@domain as PROTOCOL.krl section 5 recommends and not critical, so sshd and ssh-keygen load the list and ignore it (judged above). Parse reads it into KRL.Expires, and refuses a malformed or repeated one: read as absent, it would turn a list meant to lapse into one that never does.

The expiry is only as good as the list's authenticity: sign the list.

Merging lists: one file for sshd

sshd read a single RevokedKeys file until OpenSSH 10.3 (Debian 13 ships 10.0, Ubuntu 24.04 9.6), so a server trusting several CAs needs their lists in one. Builder.Merge adds every revocation of a parsed list (serials, key IDs, explicit keys, SHA1 and SHA256 fingerprints, under the same CAs) to the list being built; the header is the Builder's own. Merging the same lists in the same order writes the same bytes. A bitmap is read back as ranges, so a merge is bounded (4M ranges) against an alternating one.

Builder.MergeCA(k, ca, others...) merges k for ca, the way a distributor merging several CAs' lists into one needs it:

  • Kept: ca's section; any-CA sections, re-scoped to ca; and explicit keys and fingerprints. A user key ca revokes stays revoked, and ca revoking its own key revokes every certificate it signed.
  • Left out, and counted: sections for another CA, and keys or fingerprints that designate one of others, the other CAs the merged list serves.

So each list keeps its full effect on its own CA, and none can revoke another CA or its serials. One thing a single KRL cannot scope to a CA is a revoked user key: sshd checks a certificate's own public key against the list, whoever signed the certificate. A list that names the key of a user another CA certified therefore locks that user out under every CA of the merged file. Builder.MergeCAWith(k, ca, krl.MergeOptions{Others: others, DropKeys: true}) leaves out every explicit key and fingerprint except ca's own key, which reaches only ca's certificates. A distributor uses it for a list it does not trust with every CA's users (TestMergeCAWithDropKeysKeepsOnlyTheCAsOwnKey, TestOracleMergeCAWithDropKeys). MergeCA(k, ca, others...) is unchanged: it is MergeCAWith with Others alone. v0.3.0 also dropped explicit keys, fingerprints and any-CA sections: a CA revoking a compromised user key, or itself, revoked nothing once merged. A security audit found it; ssh-keygen now judges both cases (TestOracleMergeCA, TestOracleMergeCAKeepsACAsOwnKeyRevocations).

No signatures

OpenSSH never exposed a way to write the KRL signature section and no longer verifies it. Parse reads a KRL that has one, skips it as krl.c does and sets Signed, which says nothing about authenticity. Builder never writes one. A KRL's integrity comes from its transport (HTTPS from an authenticated server) or from a detached SSHSIG signature (ssh-keygen -Y sign), which this package does not handle.

License

BSD-3-Clause.

Documentation

Overview

Package krl reads, writes and checks OpenSSH key revocation lists (KRLs), the binary format of OpenSSH's PROTOCOL.krl that sshd loads through RevokedKeys and that ssh-keygen -k writes and ssh-keygen -Q queries. golang.org/x/crypto/ssh has no support for them.

A KRL revokes certificates by serial number (as lists, ranges or bitmaps) or by key ID, each under one CA key or under any CA, and revokes plain keys by their blob or by the SHA-1 or SHA-256 hash of their blob.

k, err := krl.Parse(data)
if err != nil { ... }
if k.IsRevoked(cert) { refuse the login }

b := krl.NewBuilder(version, "revoked by the bridge")
b.RevokeSerial(caKey, 42)
data, err := b.Marshal(time.Now())

Judged by ssh-keygen

Parse, IsRevoked and Builder follow krl.c, OpenSSH's own implementation, and the tests hold them to it: KRLs written by ssh-keygen -k are parsed here and every certificate and key gets the same answer from IsRevoked as from ssh-keygen -Q; KRLs written by Builder are accepted by ssh-keygen -Q, which gives the same answers again and dumps the same revocations as for the KRL it wrote itself.

Builder chooses between list, range and bitmap encodings with krl.c's cost model, so its output is usually byte for byte what ssh-keygen writes for the same revocations. It departs from it on purpose in one place: ssh-keygen lets a bitmap grow past the largest integer its own reader accepts (2048 bytes), and then cannot read the KRL it wrote ("bignum is too large"), which is what happens to many sparse serials close together. Builder starts a new section instead.

Expiry

Builder.SetExpires writes, in a non-critical extension section named ExtensionExpires, the time after which the list is no longer current; Parse reads it into KRL.Expires. sshd ignores it. It is what lets a reader tell a stale copy from a current one, once the list itself is authenticated.

Merging

Builder.Merge adds every revocation of a parsed KRL to the list being built, for a reader that takes one file: sshd before OpenSSH 10.3.

Integrity: no signatures

The format once had a signature section. OpenSSH never exposed a way to write it, has stopped verifying it, and recommends a detached SSHSIG signature (ssh-keygen -Y sign) instead. Parse still reads a KRL that has one, for old files, skips it as krl.c does and sets KRL.Signed; the signature is NOT verified, so Signed says nothing about authenticity. Builder never writes one. A KRL's integrity must come from how it is transported, such as HTTPS from a server the reader authenticates, or from a detached SSHSIG signature checked by the caller; this package does neither.

Differences from OpenSSH

Keys are parsed with golang.org/x/crypto/ssh, not OpenSSH's sshkey, so a CA key in a certificate section is refused or accepted by that parser. The two agree on every key type OpenSSH supports, with small exceptions: x/crypto still parses ssh-dss keys, which OpenSSH 10 no longer does, and does not refuse RSA keys shorter than 1024 bits.

Like OpenSSH, Parse allocates in proportion to what the KRL revokes, so a KRL should come from a source trusted not to send an enormous one.

Index

Constants

View Source
const ExtensionExpires = "expires@go-authn.github.io"

ExtensionExpires names a KRL extension section carrying, as a uint64 of seconds since 1970-01-01 UTC, the time after which the list is no longer current -- what a CRL's nextUpdate is (RFC 5280, 5.1.2.5). It is written not critical, so sshd, which knows no extension, loads the list and ignores it; PROTOCOL.krl section 5 recommends the name@domain form.

It says nothing unless the list is authenticated: a list fetched over a channel anyone can write to carries whatever expiry the writer chose. Sign the list (SSHSIG, which PROTOCOL.krl recommends over its own signature section) and check both.

Variables

This section is empty.

Functions

This section is empty.

Types

type Builder

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

Builder accumulates revocations and encodes them as a KRL that ssh-keygen and sshd accept. It never writes a signature section. The Revoke methods record the first invalid request and Marshal returns it. A Builder is not safe for concurrent use.

func NewBuilder

func NewBuilder(version uint64, comment string) *Builder

NewBuilder returns a Builder for a KRL with this krl_version and comment.

func (*Builder) Marshal

func (b *Builder) Marshal(now time.Time) ([]byte, error)

Marshal encodes the KRL with now as its generation date. The output depends only on the revocations and on the order in which CAs were first named. Serials are encoded as lists, ranges and bitmaps chosen by the cost model of krl.c, except that a bitmap is never let grow past what OpenSSH can read back (ssh-keygen itself does not stop there).

func (*Builder) Merge added in v0.2.0

func (b *Builder) Merge(k *KRL)

Merge adds every revocation of k to the list being built: serials, key IDs, explicit keys and SHA1 and SHA256 fingerprints, under the same CAs. It is what serving several CAs' lists to a reader that takes one file needs -- sshd before OpenSSH 10.3 reads a single RevokedKeys file. k's header (version, date, comment, expiry) is not carried over: the merged list has the Builder's own.

func (*Builder) MergeCA added in v0.3.0

func (b *Builder) MergeCA(k *KRL, ca ssh.PublicKey, others ...ssh.PublicKey) (dropped int)

MergeCA adds what k says about the certificates ca signed, and about keys, leaving out only what reaches another CA. Of k it keeps:

  • its section for ca: serials and key IDs;
  • its sections for any CA, re-scoped to ca: serial N of any CA is, among other things, serial N of ca;
  • its explicit keys and SHA1 and SHA256 fingerprints -- a user key a CA revokes is revoked, and so is ca's own key, which revokes every certificate ca signed;

and it leaves out, counting them in dropped: its sections for another CA, and an explicit key or fingerprint that designates one of others -- the keys of the other CAs the merged list serves.

It is what a distributor merging several CAs' lists into one needs: each list keeps its full effect on its own CA's certificates, and none can revoke another CA, or lock its users out, by naming that CA's key or its serials.

What it does not stop is a list naming the public key of a user another CA certified: sshd refuses that user's certificate, whoever signed it. A distributor that does not trust a list with that merges it with MergeCAWith and MergeOptions.DropKeys.

⛔ v0.3.0's MergeCA dropped every explicit key and fingerprint, and the any-CA sections: a CA revoking a compromised user key it had certified, or its own key, revoked nothing once merged (found by a security audit).

func (*Builder) MergeCAWith added in v0.5.0

func (b *Builder) MergeCAWith(k *KRL, ca ssh.PublicKey, opt MergeOptions) (dropped int)

MergeCAWith is MergeCA with options; MergeCAWith(k, ca, MergeOptions{Others: others}) is MergeCA(k, ca, others...). What it leaves out is counted in dropped.

func (*Builder) RevokeKey

func (b *Builder) RevokeKey(key ssh.PublicKey)

RevokeKey revokes a key the way ssh-keygen -k does for a public key line of its specification (ssh_krl_revoke_key): a plain key is listed as an explicit key, which also revokes every certificate made for it and, when it is a CA key, every certificate it signed; a certificate is revoked by its serial under the CA that signed it, or by its key ID when its serial is 0.

func (*Builder) RevokeKeyID

func (b *Builder) RevokeKeyID(ca ssh.PublicKey, id string)

RevokeKeyID revokes the certificates with this key ID signed by ca, or by any CA when ca is nil. An ID cannot contain a NUL.

func (*Builder) RevokeSerial

func (b *Builder) RevokeSerial(ca ssh.PublicKey, serial uint64)

RevokeSerial revokes the certificate with this serial signed by ca, or by any CA when ca is nil. Serial 0 cannot be revoked: it is what a CA writes when it names no serial, and OpenSSH refuses it.

func (*Builder) RevokeSerialRange

func (b *Builder) RevokeSerialRange(ca ssh.PublicKey, lo, hi uint64)

RevokeSerialRange revokes the certificates with serials lo through hi, both included, signed by ca, or by any CA when ca is nil. lo must not be 0 nor greater than hi.

func (*Builder) SetExpires added in v0.2.0

func (b *Builder) SetExpires(t time.Time)

SetExpires records t, in the ExtensionExpires extension, as the time after which the list is no longer current. Zero, the default, writes no expiry. t is kept to the second.

type KRL

type KRL struct {
	// Version is krl_version, a number that grows each time the list
	// changes.
	Version uint64
	// GeneratedDate is when the list was generated, in UTC.
	GeneratedDate time.Time
	// Flags is the header's flags word. PROTOCOL.krl defines none.
	Flags uint64
	// Comment is the free-form comment of the header.
	Comment string
	// Expires is when the list stops being current, from the
	// ExtensionExpires extension; zero when the list does not say. sshd
	// ignores it -- the extension is not critical -- and a verifier that
	// enforces freshness reads it here.
	Expires time.Time
	// Signed reports that the list carried a signature section. The
	// signature is skipped, never verified: see the package documentation.
	Signed bool
	// contains filtered or unexported fields
}

KRL is a parsed key revocation list. The zero value revokes nothing. A KRL is not modified by IsRevoked and may be shared between goroutines.

func Parse

func Parse(b []byte) (*KRL, error)

Parse decodes a KRL in the format of OpenSSH's PROTOCOL.krl, refusing what ssh-keygen and sshd refuse: a bad magic or format version, truncated or trailing data, an unknown section or certificate subsection, an unknown extension marked critical, a CA key OpenSSH cannot parse, serial 0, an inverted serial range, a bitmap larger than OpenSSH reads or one that wraps past 2^64-1. Unknown extensions not marked critical are ignored. A signature section is skipped and recorded in Signed.

func (*KRL) IsRevoked

func (k *KRL) IsRevoked(key ssh.PublicKey) bool

IsRevoked reports whether key is revoked by k, with the semantics of ssh_krl_check_key in OpenSSH's krl.c. A plain key is revoked when its SHA-1 or SHA-256 fingerprint or its blob is listed. A certificate (*ssh.Certificate) is revoked when its public key is revoked that way, when its key ID or its serial is revoked under the CA that signed it or under any CA, or when that CA key is itself revoked. Serial 0, what a CA writes when it names none, is never revoked by serial.

Key IDs are compared as C strings, as OpenSSH does: an ID stops at its first NUL. OpenSSH refuses a certificate whose ID has an embedded NUL; golang.org/x/crypto/ssh does not, so such a certificate is judged by the part before the NUL.

type MergeOptions added in v0.5.0

type MergeOptions struct {
	// Others are the keys of the other CAs the merged list serves, as
	// MergeCA's others: their sections, and an explicit key or fingerprint
	// that designates one of them, are left out.
	Others []ssh.PublicKey
	// DropKeys leaves out every explicit key and fingerprint except those
	// designating ca itself. A revoked plain key cannot be scoped to one
	// CA: sshd checks a certificate's own public key against the list,
	// whoever signed the certificate, so a key one CA's list names locks
	// out that key's holder under every CA the merged list serves. A
	// distributor sets it for a list it does not trust with that; ca
	// revoking its own key, which reaches only the certificates ca signed,
	// is kept.
	DropKeys bool
}

MergeOptions says what MergeCAWith leaves out of a list besides what MergeCA does.

Jump to

Keyboard shortcuts

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