I spent the weekend testing post-quantum cryptography in PGP. Not reading about it - actually generating keys, encrypting files, signing documents, and checking whether two independent implementations can talk to each other.
They can. The standard works. Keyservers don’t accept the keys yet. And almost nobody is using any of this.
The finding
The IETF draft draft-ietf-openpgp-pqc defines hybrid algorithms for OpenPGP - ML-DSA-65+Ed25519 for signing, ML-KEM-768+X25519 for encryption. Two implementations exist:
- Sequoia PGP (Rust) -
sq 1.4.0-pqc.1, backed by the Sequoia team and Red Hat - Proton’s GopenPGP (Go) -
v3.4.1-proton, the library behind Proton Mail
I generated PQC keys with each tool, encrypted messages, and decrypted them with the other:
| Sequoia decrypts | GopenPGP decrypts | |
|---|---|---|
| Sequoia encrypts | ✓ | ✓ |
| GopenPGP encrypts | ✓ | ✓ |
| Sequoia verifies | GopenPGP verifies | |
|---|---|---|
| Sequoia signs | ✓ | ✓ |
| GopenPGP signs | ✓ | ✓ |
Two teams, two languages, one spec. Interoperability is the whole point of a standard, and this one works.
What broke
Keyservers don’t accept PQC keys. When I tried uploading to keys.openpgp.org:
Error: OpenPGP v6 (RFC 9580) is not yet supported.PQC requires v6 keys (RFC 9580), and the keyserver infrastructure hasn’t caught up. So right now, you can generate and use PQC keys locally or share them out-of-band, but there’s no public distribution mechanism. This is probably the biggest practical blocker.
Everything else worked on the first try, which genuinely surprised me given these are pre-release implementations of a draft standard.
Performance
Measured on an M4 MacBook Pro (native, not Docker):
| Operation | Time |
|---|---|
| PQC key generation (4 subkeys) | 75 ms |
| Encrypt 1 MB file | 179 ms |
| Sign 1 MB file | 14 ms |
| Decrypt | ~180 ms |
For comparison, classical Ed25519/X25519 operations are roughly the same speed. The lattice math is not the bottleneck - I/O is.
The size tax
This is the part that matters for infrastructure:
| Classical (Ed25519/X25519) | PQC (ML-DSA-65/ML-KEM-768) | Factor | |
|---|---|---|---|
| Public key | 889 bytes | 47,674 bytes | 54x |
| Encrypted message | 344 bytes | 1,888 bytes | 5.5x |
| Detached signature | 271 bytes | 4,752 bytes | 17.5x |
For encrypting files - irrelevant, 48KB is nothing. For certificate transparency logs, DANE records, keyservers, or anything that aggregates thousands of signatures - this is a real problem. Package managers with PQC signature chains will need to think about bandwidth.
The ecosystem
OpenPGP (IETF) has full PQC - encryption and signing. Sequoia and GopenPGP implement it.
LibrePGP (GnuPG’s fork) supports ML-KEM for encryption only. Koch’s position is that signing “is not an important topic for now.” I think he’s wrong - code signing and package verification are arguably the most important PGP use case left, and those need quantum-resistant signatures. Encryption protects data at rest; signatures protect the supply chain.
Proton co-authored the IETF draft. When they ship PQC to 100M+ users, it’ll be the largest PQC email deployment. But today, if you generate a PQC key, GPG users cannot decrypt your messages.
The build problem
Sequoia PQC requires OpenSSL 3.5+ because ML-KEM/ML-DSA support was only added in March 2025. Debian 12 ships 3.0. Ubuntu 24.04 ships 3.0. Ubuntu 26.04 ships 3.5 and works out of the box. For older distros, you have to compile OpenSSL from source before building Sequoia.
I packaged this into Docker images so nobody else has to deal with it:
docker pull ghcr.io/detrin/pqc/sequoia:latestOn Ubuntu 26.04, you can also build natively (it ships OpenSSL 3.5):
sudo apt install libssl-dev pkg-config capnproto clang libclang-devcargo install sequoia-sq --version 1.4.0-pqc.1 \ --locked --no-default-features --features crypto-opensslFull walkthrough: Sequoia PQC
Here is the complete workflow - keygen, encrypt, decrypt, sign, verify:
SQ="docker run --rm -v $(pwd):/data ghcr.io/detrin/pqc/sequoia"
# 1. Generate a post-quantum key pair$SQ key generate \ --own-key \ --name "Your Name" \ --without-password \ --profile rfc9580 \ --cipher-suite mldsa65-ed25519 \ --output /data/key.pgp \ --rev-cert /data/key.rev
# 2. Extract public certificate (share this with others)$SQ key delete \ --cert-file=/data/key.pgp \ --output=/data/key.pub.pgp
# 3. Encrypt a fileecho "confidential data" > secret.txt$SQ encrypt \ --for-file /data/key.pub.pgp \ --without-signature \ --output /data/secret.pgp \ /data/secret.txt
# 4. Decrypt$SQ decrypt \ --recipient-file /data/key.pgp \ --output /data/decrypted.txt \ /data/secret.pgp
# 5. Sign a document (detached signature)$SQ sign \ --signer-file /data/key.pgp \ --signature-file /data/doc.sig \ /data/secret.txt
# 6. Verify$SQ verify \ --signer-file /data/key.pub.pgp \ --signature-file /data/doc.sig \ /data/secret.txtAlice and Bob: cross-implementation communication
This is where it gets interesting. Alice uses Sequoia (Rust), Bob uses pqcrypt (Go, built on Proton’s GopenPGP). Different tools, same standard - and they interoperate.
# Alice generates her key with Sequoiadocker run --rm -v $(pwd):/data ghcr.io/detrin/pqc/sequoia key generate \ --own-key \ --name "Alice" \ --without-password \ --profile rfc9580 \ --cipher-suite mldsa65-ed25519 \ --output /data/alice.pgp \ --rev-cert /data/alice.rev
# Bob generates his key with pqcrypt (Go)docker run --rm -v $(pwd):/data ghcr.io/detrin/pqc/gopenpgp keygen \ --name "Bob" \ --pqc \ --output /data/bob
# Alice encrypts a message for Bob using his public keydocker run --rm -v $(pwd):/data ghcr.io/detrin/pqc/sequoia encrypt \ --for-file /data/bob.pub.asc \ --without-signature \ --output /data/for-bob.pgp \ /data/secret.txt
# Bob decrypts with pqcrypt - different implementation, works finedocker run --rm -v $(pwd):/data ghcr.io/detrin/pqc/gopenpgp decrypt \ --key /data/bob.key.asc \ --input /data/for-bob.pgp \ --output /data/bob-reads.txt
# Bob signs a response with pqcryptdocker run --rm -v $(pwd):/data ghcr.io/detrin/pqc/gopenpgp sign \ --key /data/bob.key.asc \ --input /data/bob-reads.txt
# Alice verifies Bob's signature with Sequoiadocker run --rm -v $(pwd):/data ghcr.io/detrin/pqc/sequoia verify \ --signer-file /data/bob.pub.asc \ --signature-file /data/bob-reads.txt.sig \ /data/bob-reads.txtTwo implementations, two languages, two independently generated PQC keys - full interoperability confirmed.
Verify it yourself
I signed the following message with my PQC key using:
echo "This message was signed with a post-quantum key (ML-DSA-65+Ed25519) on 2026-05-30." > message.txtsq sign --signer-file key.pgp --cleartext message.txt > signed-message.txtHere is the result - a real cleartext-signed message from my PQC key. You can verify it:
-----BEGIN PGP SIGNED MESSAGE-----
This message was signed with a post-quantum key (ML-DSA-65+Ed25519) on 2026-05-30.
-----BEGIN PGP SIGNATURE-----
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YD8X-----END PGP SIGNATURE-----To verify, download my public key and run:
curl -O https://raw.githubusercontent.com/detrin/pqc/main/keys/daniel-herman.pub.pgpsq verify --signer-file daniel-herman.pub.pgp --cleartext signed-message.txtOutput: Authenticated signature made by B9ABC16E... (Daniel Herman) - 1 authenticated signature.
My key fingerprint: B9ABC16EC9A26EE929EEA07860A15523A3F3362853D38D14ACC24A1B55015DB2
”PGP is dead, just use Signal”
I hear you. For person-to-person messaging, PGP lost. But PGP isn’t just email:
- Code signing (git commits, package managers, firmware)
- File encryption at rest (backups, archives that outlive you)
- Document signing (legal, compliance)
These don’t have Signal alternatives. They need PQC because the documents they protect have 10-30 year lifetimes - well within the “harvest now, decrypt later” threat window.
How ML-KEM actually works
ML-KEM (FIPS 203) is a Key Encapsulation Mechanism. It does not encrypt data directly - it establishes a shared 32-byte secret between two parties, which then serves as the AES-256 key for symmetric encryption.
The security is based on the Module Learning With Errors (M-LWE) problem. The core idea:
Key generation produces a public matrix and computes:
where is the secret key vector and is a small random error vector sampled from a centered binomial distribution. The public key is . The private key is .
Encapsulation (sender side) picks a random vector and computes:
where is the message to encapsulate (a random shared secret), and , are fresh small noise terms. The ciphertext is .
Decapsulation (recipient side) computes:
The derivation: since , we get , and the terms cancel, leaving only the message plus small noise.
The noise terms are bounded: , so rounding recovers exactly. But without knowing , an attacker sees only noisy linear equations and cannot separate signal from noise.
The visualization shows the key intuition. Left: in classical crypto (RSA, ECC), values lie on a clean algebraic structure - Shor’s algorithm exploits this periodicity. Right: in ML-KEM, each value (red) is displaced from its lattice position (blue ghost) by random noise e. The legitimate recipient knows s and can cancel the noise. An attacker would need to solve the M-LWE problem - finding s given only A and t = A·s + e - which no known quantum algorithm can do efficiently.
ML-KEM-768 parameters (from FIPS 203):
- Polynomial ring: , with
- Module rank (hence “768” = )
- Public key: 1,184 bytes
- Ciphertext: 1,088 bytes
- Shared secret: 32 bytes
Why RSA falls and lattices don’t
RSA relies on factoring: . Shor’s algorithm reduces this to quantum period-finding in the group - a clean algebraic structure that the Quantum Fourier Transform exploits in polynomial time .
ML-KEM relies on M-LWE: given , recover . There is no group structure, no period, no hidden subgroup. The noise destroys any algebraic regularity. The best known quantum attack is Grover-accelerated lattice sieving, which provides only a quadratic speedup - reducing security from to , which is still computationally infeasible.
The hybrid construction in OpenPGP PQC combines both:
An attacker must break both X25519 (classical) and ML-KEM (post-quantum) to recover the key.
Should you use this today?
Yes, if you’re encrypting data that needs to stay secret for 10+ years, or signing packages where you want the signatures to remain valid past 2030.
No, if you need GPG compatibility, keyserver distribution, or are waiting for the final RFC.
The honest timeline: working today → RFC ratified 2026-2027 → Proton ships to users 2027-2028 → NIST mandate 2029.
We’re in the “works but nobody uses it” phase.
Repo: github.com/detrin/pqc - Docker images, interop tests, and a Go CLI wrapper.