An OpenPGP "key" is not a key. It is a transferable public key — a certificate: a primary key packet plus identities, subkeys, and the self-signatures that bind them together. This article covers what that structure actually contains, how fingerprints are computed in v4 and v6 and why the SHA-1 in v4 is a real problem with a narrow blast radius, why key IDs must never be treated as identifiers, how subkey binding and cross-certification prevent a specific class of key-theft attack, and the full string-to-key story from the 1990s Simple S2K through Argon2 and AEAD-protected secret keys.
1. What a certificate actually is
When someone sends you "their PGP key," what arrives is a packet stream in a specific canonical order:
Fig. 1 — Order matters for parsing; signatures matter for trust. Position in the stream conveys association, but only a verified signature conveys authorization.
The critical property: packet order expresses association, not authorization. A subkey packet appearing after a primary key does not mean the primary key holder authorized it. Only a valid subkey binding signature does. An implementation that reads certificates positionally and skips signature verification will happily import a certificate in which an attacker has appended their own encryption subkey — and will then encrypt to the attacker. This is not theoretical; permissive certificate import has been a recurring bug class.
2. The public key packet
2.1 Version 4
04 # version 4 5F 8A 2C 10 # creation time, 4-octet Unix seconds — part of the fingerprint! 01 # public-key algorithm ID (1 = RSA) .. algorithm-specific key material (MPIs for RSA) ..
Note that the creation time is inside the fingerprint input. This means you cannot change a key's creation timestamp without changing its fingerprint — the timestamp is cryptographically bound to the identity of the key, not metadata attached to it. This surprises people who try to "fix" a wrong system clock after generating a key.
2.2 Version 6
06 # version 6 67 A1 3B 40 # creation time 1B # algorithm ID (27 = Ed25519) 00 00 00 20 # NEW in v6: 4-octet count of key material octets (32) .. 32 octets of native-encoded Ed25519 public key ..
The one structural addition in v6 is that explicit 4-octet length prefix on the key material. It looks minor. It is not: it means a parser encountering an unknown public-key algorithm can still skip the packet correctly, because it knows how many octets the material occupies without understanding the algorithm. In v4, unknown-algorithm key material is unparseable — you cannot determine its length — which forces implementations into either rejecting the whole certificate or guessing. That single field is what makes v6 forward-compatible with algorithms nobody has invented yet, which is precisely what the post-quantum work in Article 6 needed.
3. Fingerprints
A fingerprint is the canonical identifier of a key. Both versions hash a synthetic prefix followed by the key packet contents.
3.1 Version 4 fingerprint
\[ \text{FP}_{v4} = \text{SHA-1}\big(\texttt{0x99} \,\|\, \text{len}_{16} \,\|\, \text{version} \,\|\, \text{time} \,\|\, \text{alg} \,\|\, \text{key material}\big) \]20 octets. The 0x99 is a synthetic old-format packet header for a public-key packet, and len is a 2-octet big-endian length of everything after it. The Key ID is the last 8 octets.
3.2 Version 6 fingerprint
\[ \text{FP}_{v6} = \text{SHA-256}\big(\texttt{0x9B} \,\|\, \text{len}_{32} \,\|\, \text{version} \,\|\, \text{time} \,\|\, \text{alg} \,\|\, \text{len}_{\text{material}} \,\|\, \text{key material}\big) \]32 octets. Different synthetic prefix (0x9B), 4-octet length, SHA-256. The Key ID is the first 8 octets — note the reversal from v4. Getting this backwards produces key IDs that never match anything, and it is a classic porting bug.
Practically: an attacker cannot take your existing fingerprint and construct a different key matching it (that needs a second preimage). An attacker can, with chosen-prefix collision capability, generate two keys they control that share a fingerprint — which matters for scenarios where an attacker gets a colliding key certified and then substitutes the twin. This is a real attack against certification workflows, not against fingerprint verification of a key you already hold. v6's move to SHA-256 removes the concern entirely; until you are on v6, the mitigation is that certification decisions should not rest on fingerprint equality alone.
3.3 Key IDs are not identifiers
A Key ID is 8 octets — 64 bits. A "short key ID," still displayed by some tooling, is 4 octets — 32 bits. In 2016 the Evil32 project generated colliding short key IDs for the entire strong set of the Web of Trust, demonstrating that 32-bit identifiers are trivially collidable on commodity hardware.
| Identifier | Bits | Collision work | Safe to use for… |
|---|---|---|---|
| Short Key ID | 32 | Trivial (seconds) | Nothing. Do not display, do not accept. |
| Long Key ID | 64 | ~232 birthday — hours on a GPU | Local lookup hints only, never trust decisions |
| v4 Fingerprint | 160 (SHA-1) | Chosen-prefix collision feasible for funded attackers | Verification of held keys; caution in certification flows |
| v6 Fingerprint | 256 (SHA-256) | Infeasible | Everything |
The implementation rule follows directly: key IDs are a database index, used to narrow a keyring lookup quickly. Once you have candidate keys, the actual decision must be made by verifying a signature or comparing a full fingerprint. Any code path where a key ID alone selects a key for a trust decision is a bug.
4. Subkeys, binding, and cross-certification
The standard operational pattern separates a long-lived primary key (certification only, kept offline) from short-lived subkeys for signing and encryption. This requires the primary key to authorize each subkey, via a Subkey Binding Signature (type 0x18) issued by the primary over the concatenation of the primary key packet and the subkey packet.
For subkeys capable of signing, that is not sufficient. RFC 9580 requires an additional Primary Key Binding Signature (type 0x19, the "back signature") issued by the subkey over the same data, embedded in the binding signature's unhashed subpackets.
0x18 binding with Mallory's primary key (which Mallory obviously controls). Mallory now has a certificate that claims Alice's signing subkey as its own. Signatures Alice made now appear to verify as coming from Mallory's certificate — Mallory has stolen attribution for Alice's signatures without ever touching Alice's private key.
The back signature closes this because it requires proof of possession of the subkey's private half, which Mallory does not have. Verifying
0x18 without also verifying the embedded 0x19 for signing-capable subkeys reintroduces the vulnerability in full.5. String-to-Key: turning a passphrase into a key
Secret key material at rest is encrypted under a key derived from a passphrase. S2K is that derivation. Its history is a compressed history of password hashing generally.
| ID | Type | Construction | Status in RFC 9580 |
|---|---|---|---|
| 0 | Simple S2K | H(passphrase) | MUST NOT generate. No salt: rainbow-tableable, identical passphrases produce identical keys. |
| 1 | Salted S2K | H(salt ‖ passphrase), 8-octet salt | Only for high-entropy input. Salt defeats precomputation but there is no work factor. |
| 3 | Iterated + Salted | H(repeat salt‖passphrase to a count) | Acceptable fallback if Argon2 unavailable. |
| 4 | Argon2 | RFC 9106 memory-hard KDF | Recommended. 16-octet salt, explicit passes/parallelism/memory. |
5.1 The Iterated-and-Salted encoding quirk
Type 3's iteration count is stored as a single octet, decoded into an octet count via a mantissa/exponent scheme:
\[ \text{count} = (16 + (c \,\&\, 15)) \ll \left(\frac{c}{16} + 6\right) \]Two things follow. First, the count is a count of octets hashed, not iterations of a hash — you repeatedly feed salt‖passphrase into the hash context until that many octets have been consumed, then finalize. Implementations that treat it as an iteration count are off by a factor of len(salt‖passphrase), producing keys that do not interoperate. Second, the maximum representable count is 65,011,712 octets, which sounds large and is not: on modern hardware this is a small fraction of a second, and it is the hard ceiling of the encoding. There is no way to express a stronger type-3 S2K. That ceiling is the entire argument for Argon2.
5.2 Argon2
Argon2id is memory-hard: the attacker's cost scales with memory as well as time, which specifically degrades GPU and ASIC advantage — the exact advantage that makes type-3 S2K weak. Its parameters are explicit in the packet:
04 # S2K type 4 = Argon2 .. 16 octets salt .. 03 # t = 3 passes 04 # p = 4 lanes (parallelism) 10 # m = 2^16 KiB = 64 MiB memory
6. Secret-key protection
The S2K usage octet in a secret key packet selects how the private material is protected:
| Usage | Protection | Assessment |
|---|---|---|
0 | Unencrypted | Private material in the clear. Legitimate for HSM-adjacent flows and automation, catastrophic by accident. |
253 | AEAD (key = HKDF of S2K output) | The modern choice. Tamper-evident. |
254 | CFB, with SHA-1 checksum over plaintext | Legacy. The checksum detects a wrong passphrase, not tampering. |
255 | "MalleableCFB" — CFB with a 16-bit sum check | v4 only. Named in RFC 9580 exactly as harshly as it deserves. MUST NOT generate. |
6.1 Why "malleable" is the correct word
Usage 255 protects the plaintext with a 16-bit additive checksum, encrypted alongside the data in CFB mode. CFB is malleable: flipping a ciphertext bit flips the corresponding plaintext bit in a predictable position. A 16-bit checksum has 65,536 possible values, so a blind attacker guessing has a 2-16 chance per attempt — and a non-blind attacker can often craft modifications that preserve an additive checksum outright.
The consequence is that an attacker with write access to your private keyring can modify the encrypted private key material in structured ways. Against certain algorithms this enables fault-style key recovery: an attacker who can induce controlled corruption of key parameters and then observe the resulting signatures can, for some parameter sets, recover the private key. This class of concern is why usage 253's AEAD tag — which makes any modification detectable before the material is ever used — is a genuine security improvement rather than a modernization for its own sake.
6.2 The AEAD-protected format
With usage 253, the key derivation includes context binding via HKDF, and the AEAD's associated data includes the public key packet. That binding matters: it means the encrypted private material is cryptographically tied to the specific public key it belongs to, so an attacker cannot lift the encrypted secret blob from one key and splice it into another certificate. This is the key-overwriting protection that was one of the working group's stated motivations for the v6 work — and one of the changes the LibrePGP fork disputes (Article 6).
7. Expiration, revocation, and the offline primary pattern
Neither expiration nor revocation is a property of the key packet. Both live in signatures:
- Expiration is the Key Expiration Time subpacket in the primary key's self-signature, expressed as seconds after the key creation time — not an absolute date. Extending an expiry means issuing a new self-signature, which is why expiry is a soft renewable commitment rather than a hard deadline.
- Revocation is a signature of type
0x20(key revocation) or0x28(subkey revocation), which can carry a machine-readable reason code — key compromised, superseded, no longer used. The reason matters semantically: "superseded" means past signatures remain valid, "compromised" means they do not. - Designated revokers let a key authorize another key to revoke it, which is the only real answer to "what if I lose the primary key." Set this up before you need it, because you cannot add it afterwards without the primary key.
8. Algorithm inventory as of RFC 9580
| ID | Algorithm | Encoding | Status |
|---|---|---|---|
| 1 | RSA (encrypt or sign) | MPI | Supported; ≥3072-bit for new keys |
| 2, 3 | RSA encrypt-only / sign-only | MPI | Deprecated — the split was always a mistake |
| 16 | Elgamal (encrypt-only) | MPI | Legacy; parameter-validation pitfalls |
| 17 | DSA | MPI | Legacy; nonce-reuse fragility, avoid |
| 18 | ECDH | OID + MPI + KDF params | Supported |
| 19 | ECDSA | OID + MPI | Supported |
| 22 | EdDSALegacy | OID + MPI | Deprecated in favor of ID 27 |
| 25 | X25519 | Native, 32 octets | MTI in RFC 9580 |
| 26 | X448 | Native, 56 octets | Supported |
| 27 | Ed25519 | Native, 32 octets | MTI in RFC 9580 |
| 28 | Ed448 | Native, 57 octets | Supported |
The direction of travel is clear: MPI-encoded, parameter-heavy algorithms out; fixed-length, misuse-resistant curve algorithms in. The post-quantum additions in RFC 9980 continue exactly this pattern with native encodings and no negotiable parameters.
9. Implementation checklist
| Check | Prevents |
|---|---|
| Verify every subkey binding signature before using a subkey | Attacker-appended encryption subkeys |
| Verify the embedded back signature (0x19) for signing subkeys | Signature attribution theft |
| Never make a trust decision from a key ID | Evil32-class collision attacks |
| Reject short (32-bit) key IDs entirely, including in UI | Trivially forgeable identifiers |
| Compute v6 key IDs from the first 8 octets, v4 from the last | Silent lookup failures on v6 keys |
| Reject Argon2 paired with CFB protection | Strong KDF over malleable ciphertext |
| Refuse to generate S2K usage 255 or type 0 | Malleable and unsalted secret-key protection |
| Bind AEAD associated data to the public key packet | Secret-blob splicing between certificates |
| Treat expiry and revocation as signature state, re-evaluated per use | Stale validity caching |
| Enforce a certification-count ceiling on import | Certificate flooding DoS (see Article 5) |
FAQ
Should I migrate to v6 keys now?
It depends entirely on who you need to interoperate with. v6 is technically better in every dimension discussed here — SHA-256 fingerprints, explicit key-material lengths, AEAD secret protection, Argon2. But GnuPG's LibrePGP position means a large installed base does not consume v6 the way RFC 9580 specifies. For internal systems where you control both ends, v6. For public correspondence, v4 remains the pragmatic default in 2026. Article 6 covers this decision properly.
Can I change my key's creation date if my clock was wrong?
No. The creation timestamp is hashed into the fingerprint, so changing it produces a different key with a different fingerprint and no relationship to the original. Generate a new key and use the old one to sign the new one, establishing continuity.
Is SHA-1 in v4 fingerprints an emergency?
No, but it is a real constraint. Preimage resistance holds, so nobody can forge a key matching an existing fingerprint you already verified. Collision resistance is broken, so an attacker who controls both keys in a pair can exploit workflows where a fingerprint is used to decide equivalence between keys they supplied. Verify keys out of band with full fingerprints, and prefer v6 where you can.
Why do I need both a binding signature and a back signature?
They prove different things. The binding signature proves the primary key holder authorized this subkey. The back signature proves whoever controls the subkey's private half agreed to be bound to this primary. Without the second, anyone can claim a published signing subkey as their own.
Takeaways
- A certificate is a signed graph, not a container. Position implies association; only verified signatures imply authorization.
- Key IDs are lookup hints, never identifiers. 32-bit short IDs are forgeable in seconds and should not exist in your codebase or UI.
- The back signature requirement for signing subkeys is load-bearing, preventing signature-attribution theft that costs an attacker nothing to attempt.
- Type-3 S2K has a hard ceiling at roughly 65 MB of hashing, which is why Argon2 exists in the spec rather than as a nicety.
- Argon2 requires AEAD protection. A memory-hard KDF over a malleable ciphertext is a mismatched pair, and the spec forbids it.
- v6's explicit key-material length is the quiet enabler of algorithm agility, which is exactly what made post-quantum key material addable without a new key version.
Next in this series
Article 3 — Confidentiality: CFB, the MDC, EFAIL, and SEIPDv2: OpenPGP's non-standard CFB variant, why the MDC was a bolt-on that failed, the full EFAIL gadget mechanics, and how AEAD with chunking fixes it.
References
- RFC 9580 §5.5 — Key Material Packets — normative source for v4/v6 key packet layout and fingerprint computation.
- RFC 9106 — Argon2 — the memory-hard KDF and its parameter guidance.
- Evil32: Short Key IDs are bad news — generated colliding 32-bit key IDs for the entire WoT strong set.
- SHA-1 is a Shambles — Leurent & Peyrin — practical chosen-prefix SHA-1 collision; the paper that bounds the v4 fingerprint concern.
- SHAttered — first practical SHA-1 collision — Stevens et al., 2017.
- RFC 9580 §5.5.3 — Secret-Key Packet Formats — S2K usage octets and the AEAD-protected secret key format.
- Sequoia Web of Trust documentation — clear treatment of certification semantics that build on this article's binding signatures.