What a certificate claims, and why each claim is there¶
An OpenPGP certificate is not just a public key. It is a set of statements the key makes about itself, each carried in a signature subpacket, and each one changes how a sender behaves.
The interesting thing about these statements is that omitting one is not an error. A certificate with no algorithm preferences parses, verifies, imports and works — it just quietly receives weaker messages forever, with nothing anywhere to say so. That failure mode is why this page exists.
The two signatures¶
A certificate assembled here carries exactly two signatures, both made by the primary key through the key service:
- a positive certification (type
0x13) over the primary and the user ID — the key asserting that this identity is its own; - a subkey binding (type
0x18) over the primary and then the encryption subkey, in that order — the primary adopting the subkey.
The ordering in the binding is load-bearing: it is what ties the subkey to this certificate and no other. A binding computed over the subkey alone would verify against any primary that cared to claim it.
What each subpacket does¶
| Subpacket | Where | Why it is there |
|---|---|---|
| Signature creation time | hashed | Required by RFC 9580; a signature without one is rejected by anything that checks |
| Issuer fingerprint | hashed | Identifies the signing key unambiguously |
| Key flags | hashed | Says what each key may do — certify/sign on the primary, encrypt on the subkey |
| Preferred ciphers | hashed | Stops the sender falling back to something weaker |
| Preferred hashes | hashed | Same, for the digest |
| Features | hashed | Asks for integrity-protected encryption |
| Salt notation | hashed | Randomises the digest |
| Issuer key ID | unhashed | A hint for finding the key — not a claim |
The hashed/unhashed split is the part worth internalising. Anything in the hashed area is covered by the signature and is therefore a claim the signature makes. Anything unhashed can be changed by anyone without invalidating the signature, so it must never be trusted for a decision. The issuer key ID is unhashed because it is a 64-bit lookup hint; the issuer fingerprint is hashed because it is an assertion.
Algorithm preferences: the measurable one¶
RFC 9580 says an implementation may assume TripleDES when a certificate states no preference. GnuPG is kinder than the specification and falls back to AES-128.
Neither is what this key can do, and neither would ever be noticed — this package decrypts AES-128 and TripleDES perfectly well, so every message would simply arrive weaker than necessary with no error, no warning and no log line.
So the preferences are stated, strongest first, and the test that proves it does not check that we can parse our own output. It runs real GnuPG, has it encrypt to the certificate, and asserts which cipher gpg chose:
with the preferred-ciphers subpacket: algorithm 9 (AES-256)
with the subpacket removed: algorithm 7 (AES-128)
That second line is measured, not assumed — it comes from deleting the subpacket and re-running. A test that cannot fail is decoration, and this one was checked by making it fail.
Features: asking for integrity protection¶
The Features subpacket advertises that the certificate's holder understands integrity-protected encryption (SEIPD).
Without it, a sender is entitled to fall back to the legacy Symmetrically Encrypted Data packet, which carries no integrity check at all and is malleable — an attacker who can modify the ciphertext can make predictable changes to the plaintext. For a channel whose entire purpose is receiving vulnerability reports from strangers, accepting a malleable format would be an odd choice.
The salt: defence in depth, honestly labelled¶
Every signature carries a random salt, in the notation OpenPGP.js originated and
go-crypto emits by default. A v6 signature has a dedicated field for this; v4 —
which is what this package writes, because that is the version whose fingerprint
SessionKey can compute — does not, so the notation is the interoperable place
to put one.
It buys two things, neither of which is a live break:
- an attacker cannot control the exact bytes hashed, which is the precondition for a chosen-prefix collision;
- a fault during signing — a bitflip in an HSM mid-operation — cannot be replayed against a second signature over identical input.
Both are cheap to prevent and expensive to retrofit. The two signatures get independent salts; reusing one would leave them sharing exactly what the salt was drawn to deny.
What the salt costs¶
Assembly is no longer byte-reproducible. Assembling the same inputs twice produces different octets.
What it does not change is the recipient: fingerprints are hashed over the public-key packets alone, so a salted and an unsalted assembly of the same keys accept exactly the same messages. That distinction is asserted rather than assumed — one test checks the octets differ, and that the fingerprints do not.
If byte-reproducibility is a hard requirement — a reproducible build, or a
published certificate that must not appear to have changed when regenerated —
WithoutSignatureSalt() turns it off, and says in its own documentation what
you are giving up.
What is deliberately absent¶
- Key expiry. A certificate that expires is a certificate that stops receiving reports on a date nobody diarised. Rotation here means publishing a new certificate, which is a decision for whoever operates the address.
- Compression preferences. Not a security control in this threat model — there is no adaptive attacker to feed a compression oracle — and stating nothing lets the sender pick.
- Revocation certificates. Producing one needs the primary key, which is a key-service operation and an operational procedure, not an assembly-time artefact.