Skip to content

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:

params := recipient.KDF
params.CoordinateBytes = deriver.CoordinateBytes()

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.