The Great Certificate Size Explosion – IPsec Series, Part 7 OpenSSL 3.5+ now natively supports all three NIST post-quantum algorithms (ML-KEM, ML-DSA, SLH-DSA), and a hands-on test shows that post-quantum certificates are dramatically larger than classical ones: a self-signed ML-DSA-44 certificate is 3,987 bytes versus 326 bytes for Ed25519 and 1,043 bytes for RSA-3072, with SLH-DSA-SHA2-128f reaching 17,371 bytes. The size explosion is the key challenge for post-quantum authentication in IPsec, as larger certificates increase bandwidth and latency on the wire. In Part 6 we met the post-quantum signatures and learned why authentication has a sneakier quantum deadline than key exchange. Now let’s get tactile. We’re going to generate real post-quantum keys and certificates with our own hands, line them up next to the classics, and weigh them . The size story is the single most important thing to internalise about post-quantum authentication, and the best part is you measure it yourself with nothing fancier than ls . Everything runs in a throwaway alpine container with OpenSSL, so all you need is Docker. Tooling OpenSSL 3.5+ is the first mainstream OpenSSL with native support for all three NIST PQC algorithms ML-KEM, ML-DSA, and SLH-DSA built right in. No external plugins, no patches, no extra add-ons to install. If you’ve got 3.5 or newer, you generate post-quantum keys and certificates with plain, ordinary OpenSSL commands, the same kind you’d use for any classical key check your version with openssl version . Wait, if OpenSSL does ML-KEM, why did we use strongSwan for the key exchange? Fair question. OpenSSL is a crypto library , not a VPN; its PQC support is wired into TLS and certificates , not IKEv2. So OpenSSL is perfect for making and inspecting the keys and certificates here, but it can’t run an IKEv2 handshake. That’s why anything happening on the actual VPN wire the key exchange in Parts 4–5, and the authentication coming up in Part 8 is strongSwan’s job. Two tools, two jobs. A tiny throwaway container gives us a clean 3.5+ environment in seconds: docker run --rm -it alpine:3.22 sh Then inside you install openssl: apk add --no-cache openssl openssl version expect OpenSSL 3.5.x or newer mkdir -p /pqauth && cd /pqauth All commands below run in that same shell. Step 1: Confirm the PQC algorithms are there openssl list -signature-algorithms | grep -iE "ml-dsa|slh-dsa|ed25519" You’ll see the ML-DSA and SLH-DSA families listed, for example: { 1.3.101.112, ED25519 } @ default { 2.16.840.1.101.3.4.3.18, id-ml-dsa-65, ML-DSA-65, MLDSA65 } @ default { 2.16.840.1.101.3.4.3.20, id-slh-dsa-sha2-128s, SLH-DSA-SHA2-128s } @ default ... There they are: straight from OpenSSL’s default provider, no plugins required. Step 2: Mint a self-signed cert We’ll generate a key and a self-signed certificate for eight algorithms in one loop. Quick note on the yardstick: a self-signed certificate subject = issuer, signed with its own key is the cleanest possible comparison: it bundles exactly one public key and one signature with no external CA, so the size reflects only the algorithm’s footprint. We also output DER the compact binary encoding that actually travels on the wire , not the base64 PEM text you usually see. algs="ED25519:ed25519 EC:ecp256 RSA:rsa3072 ML-DSA-44:mldsa44 ML-DSA-65:mldsa65 ML-DSA-87:mldsa87 SLH-DSA-SHA2-128s:slh128s SLH-DSA-SHA2-128f:slh128f" for entry in $algs; do alg=${entry%%: }; name=${entry :} case "$alg" in RSA openssl genpkey -algorithm RSA -pkeyopt rsa keygen bits:3072 -out "$name.key" ;; EC openssl genpkey -algorithm EC -pkeyopt ec paramgen curve:P-256 -out "$name.key" ;; openssl genpkey -algorithm "$alg" -out "$name.key" ;; esac openssl req -x509 -new -key "$name.key" -out "$name.crt" -days 365 -subj "/CN=$name-peer" -outform DER done Notice that for the post-quantum algorithms, generating a key uses the exact same one-liner as a classical one; only the algorithm name changes. That’s the beauty of OpenSSL 3.5: post-quantum isn’t a separate, special workflow, it’s just another algorithm you pass to the same command. Step 3: Weigh them ls -la .crt | awk '{print $5" "$9}' | sort -n Brace yourself: 326 ed25519.crt 389 ecp256.crt 1043 rsa3072.crt 3987 mldsa44.crt 5516 mldsa65.crt 7474 mldsa87.crt 8139 slh128s.crt 17371 slh128f.crt Look at that jump Let’s put it in plain terms with the underlying numbers: | Algorithm | Security | Public key | Signature | Cert DER | |---|---|---|---|---| | Ed25519 | ~128-bit | 32 B | 64 B | 326 B | | ECDSA P-256 | ~128-bit | 65 B | ~70 B | 389 B | | RSA-3072 | ~128-bit | 384 B | 384 B | 1043 B | ML-DSA-44 | NIST L2 | 1312 B | 2420 B | 3987 B | ML-DSA-65 | NIST L3 | 1952 B | 3309 B | 5516 B | ML-DSA-87 | NIST L5 | 2592 B | 4627 B | 7474 B | | SLH-DSA-128s | NIST L1 | 32 B | 7856 B | 8139 B | | SLH-DSA-128f | NIST L1 | 32 B | 17088 B | 17371 B | An Ed25519 certificate is just 326 bytes . The equivalent ML-DSA-65 cert is 5516 bytes , roughly 17× larger . And SLH-DSA-128f? Its signature alone is 17088 bytes , bigger than many entire web pages. Notice SLH-DSA’s quirk too: a tiny 32-byte public key, but a gigantic signature. Whoa. Wait, why isn’t the cert just key + signature? Good catch. For ML-DSA-65, key 1952 B + signature 3309 B = 5261 B, yet the cert is 5516 B, about 255 B more. A certificate isn’t a simple concatenation; it’s a structured X.509 document embedding the key and signature alongside metadata version, serial, validity dates, issuer/subject names, algorithm OIDs, a few extensions, plus ASN.1/DER framing on every field . That overhead is roughly fixed ~230–260 B regardless of algorithm, which is why it dominates a tiny Ed25519 cert but barely registers on a chunky ML-DSA one. Why this size story matters Handshakes don’t carry one cert; they carry certificate chains plus a handshake signature . Swap a 3-cert ECDSA chain ~1.2 KB total for an ML-DSA-65 chain and you’re suddenly shipping 15–20 KB in the handshake. In IKEv2 that means the IKE AUTH exchange balloons and leans hard on fragmentation : the same pressure ML-KEM put on IKE INTERMEDIATE back in the key-exchange pillar, but now on the authentication leg.And “leaning hard on fragmentation” isn’t just a minor detail. Fragmentation was designed as an occasional fallback for the rare oversized message; post-quantum flips that so almost every handshake is large and fragmented. That brings real costs: Packet loss hurts more. An N-fragment message reassembles only if all N arrive; lose one and the whole thing retransmits. On lossy links, big PQC handshakes retransmit more and latency spikes. Middleboxes are hostile to fragments. Firewalls, NATs, and load balancers routinely drop or mishandle fragmented UDP, producing handshakes that fail in really hard-to-debug ways. Bigger pre-auth attack surface. These bytes fly before the peer has proven who it is, so a responder must buffer and reassemble fragments from a not-yet-authenticated initiator: extra state an attacker can try to overload. None of this breaks PQC auth strongSwan handles it , but it’s why “the certs got bigger” turns into round trips, retransmits, and middlebox trouble at scale. We’ll watch exactly this happen on the wire in Part 8. Step 4: Are they slow? A common worry, so before we sign anything ourselves we’ll do that in Step 5 , here’s what to expect per signature: | Algorithm | Signing | Notes | |---|---|---| | Ed25519 / ECDSA | ~instant | so fast the timing is dominated by just launching the command | | ML-DSA-44/65/87 | ~instant | sits right alongside the classics | | SLH-DSA-128f | a few ms | noticeably heavier | | SLH-DSA-128s | ~100+ ms | dramatically slower to sign | The takeaway: ML-DSA signs and verifies right alongside Ed25519 and ECDSA , while SLH-DSA’s s “small” variants are hugely slower. That slowness is exactly why SLH-DSA is reserved for things you sign rarely a root CA signs a handful of certs a year, so who cares if each takes 100 ms? rather than a busy TLS terminator doing thousands of handshakes a second. You’ll feel that difference for yourself with the time command in Step 5. Step 5: Sign, verify, tamper Certificates are about trust , and trust comes from signatures that can’t be forged. Let’s prove it. echo "transfer 1000 to tom" msg.txt openssl pkeyutl -sign -inkey mldsa65.key -rawin -in msg.txt -out msg.sig wc -c < msg.sig ~3309 bytes Pull out the public half and verify with it openssl pkey -in mldsa65.key -pubout -out mldsa65.pub openssl pkeyutl -verify -pubin -inkey mldsa65.pub -rawin -in msg.txt -sigfile msg.sig You should see Signature Verified Successfully . Note the clean split: signing used the private key, verifying used only the public key. That’s the whole point of public-key signatures: signing is private, verifying is public. Now tamper with the message and watch it fail: echo "transfer 1000 to mallory" msg.txt openssl pkeyutl -verify -pubin -inkey mldsa65.pub -rawin -in msg.txt -sigfile msg.sig Signature Verification Failure . Change a single byte and the post-quantum signature rejects it: exactly the unforgeable integrity guarantee we depend on, now resting on lattice math instead of elliptic curves. Want to feel the SLH-DSA difference? Time it: time openssl pkeyutl -sign -inkey slh128s.key -rawin -in msg.txt -out slh.sig wc -c < slh.sig ~7856 bytes Noticeably slower, and the signature more than twice the ML-DSA size. That’s the hash-based trade-off in action: ultra-cautious security, but you’d never want it on a high-volume handshake. What we just did You generated post-quantum certificates, measured the size hit with your own hands, watched ML-DSA and SLH-DSA sign and verify and reject a forgery , and felt the SLH-DSA speed penalty. The “certs get bigger” story is no longer something you read; it’s something you weighed. But certificates sitting in a folder are only half the fun. In Part 8 we hand these certs to strongSwan and use them to mutually authenticate a real IKEv2 VPN tunnel: classical ECDSA first, then bleeding-edge post-quantum ML-DSA. We’ll watch the handshake balloon and fragment exactly as predicted, and stand right at the bleeding edge where this stuff is still being built. See you there