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
- type Builder
- func (b *Builder) Marshal(now time.Time) ([]byte, error)
- func (b *Builder) Merge(k *KRL)
- func (b *Builder) MergeCA(k *KRL, ca ssh.PublicKey, others ...ssh.PublicKey) (dropped int)
- func (b *Builder) MergeCAWith(k *KRL, ca ssh.PublicKey, opt MergeOptions) (dropped int)
- func (b *Builder) RevokeKey(key ssh.PublicKey)
- func (b *Builder) RevokeKeyID(ca ssh.PublicKey, id string)
- func (b *Builder) RevokeSerial(ca ssh.PublicKey, serial uint64)
- func (b *Builder) RevokeSerialRange(ca ssh.PublicKey, lo, hi uint64)
- func (b *Builder) SetExpires(t time.Time)
- type KRL
- type MergeOptions
Constants ¶
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 ¶
NewBuilder returns a Builder for a KRL with this krl_version and comment.
func (*Builder) Marshal ¶
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
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
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
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 ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
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 ¶
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.