PQ CRYPTA PLATFORM

๐Ÿ  Main

๐Ÿงช Interactive Apps

๐Ÿ“ฐ News

๐Ÿ›ก๏ธ PQ Crypta Proxy

๐Ÿ‘ค Account

โŸจ QUANTUM ERROR PORTAL โŸฉ

Navigate the Error Dimensions

PQ Crypta Logo

Certificates Are Encoded, Not Compressed

Where zlib finds 1,059 bytes in a 3,435-byte DER chain, and what that buys a post-quantum handshake

A common objection to RFC 8879 is that it should not work. Certificates are already DER, and DER is a compact binary encoding, so what is left to squeeze? The answer, measured on this server's own chain, is about a kilobyte. DER is a canonical encoding. It is not an entropy coder, and it makes no attempt to notice that it has written the same bytes four times.

Every number below is measured on this server's live chain and reproducible with a script you can run against any origin, including your own.

The short version

A single certificate is a poor thing to compress. Roughly a third of it is cryptographically random, because the public key, the signature and both key identifiers are meant to be. Compressors are pattern matchers, and a random number offers no pattern. Feed one certificate to zlib and about ten percent comes off, which is real but not worth building a protocol around.

A chain of four is a different proposition. It repeats the same distinguished names, the same OIDs, the same extension headers and the same ASN.1 framing over and over, because DER is canonical rather than clever. It writes everything out in full every time, with the enthusiasm of a clerk who has never been told about rubber stamps. Internet Security Research Group appears three times in ours. C=US appears seven times.

Compress the four certificates separately and you save 337 bytes. Compress them as one stream, which is what RFC 8879 actually does, and you save 1,055. The extra 718 bytes are not inside any certificate. They exist only in the relationship between them, which is why compressing at issuance would collect a third of the benefit and miss the rest.

A certificate is a signed form: letterhead at the top, boilerplate in the middle, a signature at the bottom that belongs to that document alone. You cannot shorten anybody's signature. You can absolutely stop printing the letterhead four times.

The distinction that does the work

PEM is DER wrapped in Base64 and header lines, which costs roughly a third again in size for no benefit on the wire. Dropping to DER removes that. What DER does not do is look for repeated byte patterns and represent the second occurrence as a reference to the first. That is a different job, and nothing in X.509 does it.

The comparison people reach for is PNG, and it is the wrong one. PNG is already DEFLATE-compressed, which is why gzipping a PNG gains you almost nothing. DER is closer to a tightly packed struct: efficient field-by-field, entirely naive about repetition across fields. A four-certificate chain repeats itself constantly, and that repetition is what a general-purpose compressor is for.

Where the repetition actually lives

Leaf · CN=pqcrypta.com
SEQUENCE/SET framing ecdsa-with-SHA384 OID id-ecPublicKey OID KeyUsage / EKU structure issuer DN "Let’s Encrypt" SANs SCTs
934 B
Intermediate · Let’s Encrypt YE1
SEQUENCE/SET framing ecdsa-with-SHA384 OID id-ecPublicKey OID KeyUsage / EKU structure subject DN "Let’s Encrypt" CPS policy structure
655 B
ISRG Root YE
SEQUENCE/SET framing id-ecPublicKey OID KeyUsage / EKU structure DN "Internet Security Research Group" basicConstraints CA
682 B
ISRG Root X2, cross-signed by X1
SEQUENCE/SET framing DN "Internet Security Research Group" basicConstraints CA KeyUsage / EKU structure RSA signature by X1
1,140 B
recurs across certificates unique to one certificate
Every highlighted item appears more than once in the same byte stream. Object identifiers, algorithm identifiers, the ASN.1 framing itself, and the distinguished names of two organisations that appear as both subject and issuer as you walk up the chain. DER writes each of them out in full, every time.

What RFC 8879 actually does

It does not change certificates. The server takes the encoded Certificate message it was going to send, compresses that byte stream with zlib, Brotli or Zstandard, and sends a CompressedCertificate message instead. The receiver decompresses it and processes the result as an ordinary certificate message. The specification requires the output to reproduce the original message exactly, so nothing about validation, pinning or transparency changes. The only thing that changed is how many bytes crossed the network.

    CompressedCertificate, Length=2384
      Compression type=zlib (0x0001)
      Uncompressed length=3435
      Compressed length=2376, Ratio=1.445707:1

That is this server, read off the handshake with openssl s_client -tls1_3 -trace. Without -trace you will not see it: OpenSSL decompresses before it reports anything, so a compressed handshake and a plain one look identical from the outside.

The measurement, and why three numbers differ

Measured on pqcrypta.com, 2026-09-15
SpanBeforeAfterSaved
Compressed payload against the message it replaces 3,435 B 2,376 B 1,059 B
CompressedCertificate message on the wire, header included 3,435 B 2,384 B 1,051 B
Whole handshake, client-observed 4,920 B 3,871 B 1,049 B

All three are correct and they measure different spans, which is worth stating plainly because the arithmetic looks wrong otherwise. The compressor turns 3,435 bytes into 2,376, a saving of 1,059. The message carrying it costs eight bytes of header for the algorithm identifier and the uncompressed length, so what actually leaves the machine is 2,384 and the saving is 1,051. Measured end to end across the whole handshake the delta comes out at 1,049, the last two bytes being record-layer variation between connections. Quote whichever you like, but say which one it is.

Chain, plain
3,435 B
Chain, zlib
2,376 B
Handshake, plain
4,920 B
Handshake, zlib
3,871 B

What the compressor actually matched

The claim so far is that repetition is what gets collapsed. That is checkable rather than assertable. Running a DEFLATE-shaped LZ77 pass over the same 3,435-byte message (32 KB window, 258-byte maximum match, 3-byte minimum, which is what RFC 1951 allows) gives the token stream the real compressor is working from: 115 back-references and 2,045 literals, with the references covering 1,390 bytes, or 40.5% of the message.

The part that matters is where those references point. 76 of the 115 point into an earlier certificate, accounting for 1,174 of those bytes. The compressor is not tidying up each certificate in isolation; it is spending most of its effort noticing that it has seen this material before, in a different certificate.

81 B The Internet Security Research Group distinguished name, with its country and organisation attribute framing Root YE → Root X2 · dist 802
80 B The same DN again, one byte offset, as issuer rather than subject Root YE → Root X2 · dist 689
60 B i.lencr.org/ AIA URL plus the id-ce-certificatePolicies extension header YE1 → Root YE · dist 686
54 B C=US and the Let's Encrypt organisation name in full RDN form leaf → YE1 · dist 1017
48 B C=US plus the ISRG organisation name YE1 → Root YE · dist 774
47 B certificatePolicies and the start of cRLDistributionPoints Root YE → Root X2 · dist 732
38 B authorityInfoAccess with the id-ad-caIssuers OID Root YE → Root X2 · dist 732
28 B basicConstraints CA:TRUE followed by subjectKeyIdentifier Root YE → Root X2 · dist 732

None of that is a coincidence of this particular chain. Counting only fragments that can be named — specific OIDs, specific distinguished names, specific extension identifiers — 412 bytes are literal duplicates: the ISRG DN appears three times at 32 bytes each, C=US seven times, the ecdsa-with-SHA384 OID six times, i.lencr.org/ four times, and each of six X.509 extension OIDs four times. Those are the bytes DER is obliged to write out in full and a compressor is free to write once.

The ceiling, and why the headline ratio understates it

1.446:1 is a modest-sounding number, and it is the wrong number to judge the compressor by. A little over a third of this message is cryptographically random and cannot compress at all: the four signatures total 821 bytes and the four public keys 388, and random bytes are exactly what a compressor cannot help with.

1,209 B · 35.2%
2,226 B · 64.8%
signatures and public keys — incompressible structure, names, OIDs, extensions — compressible

Separating the two and compressing each on its own settles it. The high-entropy material does not merely resist compression, it grows: 1,209 bytes of signature and key become 1,220, because zlib still has to pay for its header and for coding symbols it cannot predict. Everything else compresses at nearly two-to-one.

1.446:1
Whole message, which is what the wire sees
3,435 → 2,376
1.968:1
Structure alone, which is what the compressor is actually good at
2,226 → 1,131
0.991:1
Signatures and keys alone, which get bigger
1,209 → 1,220

This chain carries an unusually large incompressible share because of the fourth certificate: ISRG Root X2 cross-signed by X1 carries a 512-byte RSA signature, four times the size of the ECDSA signatures above it. On chains without an RSA cross-sign the incompressible fraction runs closer to 18–20% — GitHub's is 18.4%, Facebook's 19.6% — and the headline ratio is correspondingly better even though the structural saving is much the same.

Proof that the chain, not the certificate, is the unit

One more measurement makes the point cleanly. Compress each certificate on its own and the four outputs total 3,074 bytes. Compress them as one stream, which is what RFC 8879 does, and the result is 2,356. The 718-byte difference is purely cross-certificate redundancy — material the compressor can only find because the certificates share a window.

each certificate compressed alone : 864 + 579 + 603 + 1028 = 3,074
all four compressed as one stream : 2,356
                                    718 bytes found only across certificates

That is also the argument against the intuition that a per-certificate scheme, or compressing certificates at issuance, would do the same job. It would capture the 3,411 → 3,074 part and miss most of the rest.

A correction to the usual telling

This argument is often stated as “a single certificate is incompressible, and sometimes it even grows.” Neither half holds up when measured. Every certificate in this chain compresses on its own, and not one of them grew:

leaf, pqcrypta.com934 → 8647.5%
Let's Encrypt YE1655 → 57911.6%
ISRG Root YE682 → 60311.6%
ISRG Root X21,140 → 1,0289.8%

A certificate names both its issuer and its subject, which share attribute framing; it lists six extension OIDs; it repeats C=US. That is enough internal boilerplate to pay zlib's framing cost several times over.

The growth does happen, but only to material that has had all its structure removed first. That is the 1,209 → 1,220 figure above, and it applies to the isolated signatures and keys, never to a real certificate.

So compressing at issuance is not a dead idea, it is a half idea. It collects 337 of the 1,055 bytes available on this chain, which is 32%, and misses the 68% that only appears once the certificates are stacked.

All three algorithms, on the same bytes

AlgorithmIDOutputRatioSaved
zlib (deployed here)12,376 B1.446:11,059 B
brotli, quality 522,362 B1.454:11,073 B
brotli, quality 1122,274 B1.511:11,161 B

Brotli at maximum quality is worth another 102 bytes over zlib, about 4% of the message. That is a real gain and it is not why zlib is deployed here; the reason is stated below and it is about what the library can produce, not about which algorithm is better.

The chain is not padded

Before enabling any of this the obvious question was whether the chain was simply carrying a root it did not need to send. It is not. Those four certificates are Let’s Encrypt’s cross-signed ECDSA path: leaf, YE1, ISRG Root YE, and ISRG Root X2 cross-signed by the older and more widely trusted X1. The last one exists so that clients without X2 in their trust store can still build a path. Dropping it would break exactly the older clients least able to cope. Compression was the saving available here. Trimming was not.

Why this is worth more on a post-quantum handshake

The interesting part is that these two things are unrelated and pull in opposite directions. A hybrid key exchange enlarges the handshake because the ML-KEM key share is large. Certificate compression shrinks it because the chain is repetitive. Neither has any bearing on the other, which is precisely why one can pay for the other.

X25519MLKEM768 key share, against a classical X25519 exchange about +1,100 B
RFC 8879 zlib certificate compression −1,049 B
Net effect on the handshake roughly break-even

Post-quantum key exchange takes the bytes with one hand and certificate compression gives most of them back with the other. That matters most where it always matters: lossy paths, small MTUs, and the first flight of a connection, where an extra packet is an extra chance to lose one.

Turning it on is not one switch

Two TLS stacks terminate traffic here, and they wanted very different things.

On the OpenSSL side, SSL_CTX_set1_cert_comp_preference tells the library which algorithms you would like. It returns success. It changes nothing on the wire. The call that does the work is SSL_CTX_compress_certs, and until it runs the server keeps sending the same uncompressed chain with complete confidence. Neither function is bound by the openssl or openssl-sys Rust crates, so both are raw FFI.

On the rustls side, which serves QUIC and HTTP/3, there is no call at all. Adding the brotli and zlib crate features is the entire change, because rustls populates its compressor list from the compiled features. One stack needed two unbound C functions and a packet capture to confirm. The other needed a comma in Cargo.toml.

zlib and not Brotli, on the TCP side. Brotli compresses this chain slightly better, 2,257 bytes against 2,376 in local testing. It is not offered here because these OpenSSL builds carry -DZLIB and nothing else, and advertising an algorithm the library cannot produce is worse than offering one fewer. It is also what clients ask for: OpenSSL’s own s_client offers zlib alone.

Checking any server, including yours

The HTTP/3 and QUIC analyzer reads this from the handshake for any origin you point it at and reports one of three things, which are deliberately not collapsed into two.

ReportedMeaning
zlib or brotliThe origin compressed its chain, with that algorithm.
NoneThe origin was offered the option and declined. A finding about its configuration, not a gap in the scan.
Not measuredNo TLS handshake completed, so nothing is claimed.

The middle row is the one worth having. A blank field and a measured absence look identical from outside, and only one of them is information.

Here is that scan, run against seven origins. The column that makes it evidence rather than anecdote is the middle one: what the probe itself offered. A result of “no compression” only means anything if the server was asked.

Origin Probe offered (control) Server sent
pqcrypta.comzlibCompressedCertificate, zlib
api.pqcrypta.comzlibCompressedCertificate, zlib
mail.fated.orgzlibCompressedCertificate, zlib
www.facebook.comzlibCompressedCertificate, zlib
github.comzlibplain Certificate
www.cloudflare.comzlibplain Certificate
www.google.comzlibplain Certificate

Of the four origins here that are not ours, one compresses. The feature is standardised, implemented in every major TLS library, and still mostly switched off.

A note on how easy this is to get wrong. The first run of this scan reported “no compression” for all seven origins, including the three that demonstrably do compress. The probe had been given a -cert_comp flag, which does not exist; s_client errored out and every row came back empty.

A broken probe does not announce itself. It produces a clean, uniform, entirely wrong negative, and the more hosts it touches the more convincing the result looks. That is why the control column is on the table and not just in the method: the run is only readable because some rows came back positive under the same probe. Certificate compression is on by default in OpenSSL 3.5 and needs no flag to request — -no_tx_cert_comp and -no_rx_cert_comp exist to turn it off.

Reproduce all of it

Every figure on this page comes from one script, and it runs against any TLS 1.3 server, not just this one. It pulls the chain, rebuilds the Certificate message per RFC 8446 4.4.2, compresses it per RFC 8879, separates the incompressible material from the structure, and then reads the live handshake to check its arithmetic against what the server actually sent.

verify-cert-compression.py Download the script
$ python3 verify-cert-compression.py pqcrypta.com

  chain of 4 certificates
    1.   934 B  CN=pqcrypta.com
    2.   655 B  C=US, O=Let's Encrypt, CN=YE1
    3.   682 B  C=US, O=ISRG, CN=Root YE
    4.  1140 B  C=US, O=Internet Security Research Group, CN=ISRG Root X2

    DER total            3411 B
    TLS framing            24 B
    Certificate msg      3435 B

  compressed (this script, offline)
    zlib    (1)        2376 B   ratio 1.446:1   saved 1059
    brotli  (2)        2274 B   ratio 1.511:1   saved 1161

  where the limit comes from
    signatures + keys    1209 B  (35.2% of the message)
      compressed         1220 B   ratio 0.991:1  -- it GROWS
    everything else      2226 B
      compressed         1131 B   ratio 1.968:1

  on the wire
    probe offered      zlib
    server sent        CompressedCertificate, zlib
    3435 -> 2376 B  (ratio 1.445707:1)
    reproduced exactly: this script got 2376 B too

The last line is the point of the whole exercise. The script never sees the server's compressor; it reconstructs the message from the public chain and compresses it locally, and lands on the same 2,376 bytes the server put on the wire. Nothing on this page has to be taken on trust.

It is honest about disagreement too. Run it against Facebook and it reports 2,120 bytes against the 2,132 observed, and says so: a 12-byte gap because their compressor is not configured identically. Run it against an origin that declines, and it prints what it offered before it prints what it got back.

Only openssl is required. brotli and zstandard are optional and add algorithms 2 and 3 to the comparison when installed.

What this does not do