Read a certificate¶
Recover the parameters a message's key was derived from, with the certificate's subkey binding verified rather than assumed.
recipient, notes, err := certificate.Parse(der)
if err != nil {
return err
}
for _, note := range notes {
log.Printf("certificate note: %v", note)
}
Parse returns three values: the recipient, a Findings slice of non-fatal
notes, and an error. notes records what the parser worked around rather than
failed on — an older subkey it fell back past, a signature of a version it could
not evaluate — so a caller can surface them without the parse itself failing. It
is often empty; ignore it only deliberately.
recipient carries the encryption subkey's fingerprint, its key id, and the
KDFParams that SessionKey needs. Everything except CoordinateBytes, which
is a property of the curve as your key service reports it rather than something
the certificate states:
Both encodings¶
Parse takes binary packets. A certificate published on a web page is
armoured, so dearmour it first — armor.Decode from go-crypto, or whatever
your application already uses. The core does not carry an armour codec for one
caller.
Why it verifies¶
Anyone can append a public subkey packet to a certificate they do not own. Nothing about the format prevents it; what prevents it being believed is the binding signature the primary makes over the subkey.
A parser that skips that check will happily hand back an attacker's key, and a
sender directed at it encrypts to the attacker while believing it is talking to
the certificate's owner. So Parse verifies the binding against the primary
before returning anything, and refuses a subkey that has none.
This matters most when the certificate did not come from you — fetched from WKD, pasted into a ticket, downloaded from a page over a connection you did not pin.
What it checks about standing¶
Parse evaluates the certificate as of a time — time.Now here, or an instant
you supply through ParseAt — and refuses one whose primary key the holder has
revoked or whose primary lifetime has lapsed. A revoked subkey is refused even
though its binding still verifies; if that leaves no ECDH candidate, the error
wraps encryption.ErrRevoked, and an expired primary wraps
encryption.ErrExpired, so a caller can tell "withdrawn or lapsed, fetch a
current certificate" from "this certificate is broken".
This is standing at the moment you parse, and the default is the safe one for a sender: nothing new should be encrypted to a withdrawn certificate. A caller decrypting a message sent before the certificate was withdrawn needs the opposite — the key still opens that message — so it opts in:
recipient, notes, err := certificate.Parse(der, certificate.AllowWithdrawn())
if err != nil && !errors.Is(err, encryption.ErrRevoked) && !errors.Is(err, encryption.ErrExpired) {
return err // a real failure
}
// On ErrRevoked/ErrExpired, recipient is populated: warn loudly, then proceed.
AllowWithdrawn returns the recipient alongside the standing error rather
than in place of it, so the error is never suppressed — the caller decides,
loudly, whether to go on.
What it does not check¶
- Identity self-certifications. This reads a certificate to find out how to decrypt; the user ID plays no part in the derivation. If you need to know that a name belongs to a key, that is a web-of-trust question and this is not the tool for it. (The primary's own expiry, which rides on one, is read.)
- Anything but ECDH subkeys. They are the only ones this module can derive against, so others are ignored and a certificate with none is refused.
Errors¶
| Sentinel | Means |
|---|---|
encryption.ErrMalformed |
Not a certificate, truncated, more than one certificate, or no ECDH subkey |
encryption.ErrUnsupported |
A version or algorithm this package does not implement — a v6 key, or a non-RSA primary |
encryption.ErrIntegrity |
A binding signature is present and does not verify |
encryption.ErrRevoked |
The holder has revoked the certificate or the only ECDH subkey |
encryption.ErrExpired |
The primary key's own lifetime has lapsed |
Walking the packets yourself¶
If you need something Parse does not return, encryption.ParsePacket reads
one packet and hands back the rest, so a sequence is an ordinary loop:
for rest := der; len(rest) > 0; {
pkt, err := encryption.ParsePacket(rest)
if err != nil {
return err
}
// pkt.Tag, pkt.Body …
rest = pkt.Rest
}
Both header forms are handled — the RFC 9580 form a modern implementation writes, and the legacy CTB that older senders still emit. Partial and indeterminate lengths are refused rather than guessed at.