ML-DSA Signature Size and the 4 KB Cookie Limit (Keycloak Prep)

Guilliano Molaire Guilliano Molaire 10 min read

Last updated: September 2026

An ML-DSA signature is 2,420 bytes at the smallest parameter set and 4,627 bytes at the largest, against 256 bytes for an RS256 signature over a 2,048-bit key. Base64url encoding adds a third on top. The practical consequence for OIDC is that any design storing a signed ID token in a browser cookie runs into the 4 KB cookie ceiling the moment you switch signing algorithms, and the token does not get smaller because you trimmed a claim. The fix is structural: keep tokens out of cookies and keep session state on the server.

Those sizes come from FIPS 204, the NIST Module-Lattice-Based Digital Signature Standard that specifies ML-DSA. The three parameter sets are fixed, published, and not negotiable at runtime.

Key takeaways

  • ML-DSA-44, 65 and 87 produce signatures of 2,420, 3,309 and 4,627 bytes (FIPS 204).
  • Base64url-encoded, that is roughly 3.2 KB to 6.2 KB of signature alone in a JWS.
  • The browser cookie ceiling is about 4 KB including the name and attributes, so ML-DSA-65 and 87 tokens cannot fit in one cookie at all.
  • Keycloak’s PQC work is tracked openly: ML-DSA key loading is targeted at 26.8, the readiness epic at 27.0, and the FAPI policy executor still rejects ML-DSA outright.

What is the ML-DSA signature size for 44, 65 and 87?

It is 2,420, 3,309 and 4,627 bytes, which is nine to eighteen times an RS256 signature depending on parameter set. FIPS 204 fixes ML-DSA-44 at a 2,420-byte signature with a 1,312-byte public key, ML-DSA-65 at 3,309 and 1,952 bytes, and ML-DSA-87 at 4,627 and 2,592 bytes. An RS256 signature over a 2,048-bit key is 256 bytes, and an ES256 signature is 64.

Algorithm Signature (bytes) Public key (bytes, modulus or raw) Signature, base64url (chars)
ES256 64 64 86
RS256 (2048-bit) 256 256 342
ML-DSA-44 2,420 1,312 3,227
ML-DSA-65 3,309 1,952 4,412
ML-DSA-87 4,627 2,592 6,170

Signature and key sizes are the FIPS 204 parameter sets, cross-checked against the Open Quantum Safe project’s liboqs algorithm table in 2026. Base64url figures are the unpadded encoded length, which is what actually travels in a JWS compact serialization. The RSA figure is the modulus alone, since a JWK also carries the exponent.

The number that catches people out is the last column, because JWTs are transported as text. A 2,420-byte signature is not 2.4 KB on the wire, it is 3.2 KB, and the ratio is fixed at four characters per three bytes no matter how you tune anything else. Claim trimming works on the payload, which for a typical ID token runs 400 to 900 bytes encoded, and no amount of it gets you out of a 6 KB signature.

Because it is the smallest hard wall in the chain, and it is enforced by the client, which means you cannot raise it. RFC 6265, published in 2011, tells user agents to support at least 4,096 bytes per cookie, measured across the name, value and attributes, and browsers have universally treated that floor as the ceiling. In practice the major browsers enforce it on name plus value and cap attributes separately, so your usable payload is 4,096 minus the length of the cookie name.

Run the arithmetic on a modest ID token. A stock Keycloak ID token payload, the one carrying iss, aud, sub, sid, at_hash, acr, preferred_username, email and the usual timestamps, encodes to around 630 characters, and a header with alg, typ and a UUID kid adds about 100. Call it 730 before the signature. Signed with RS256 that is a token of about 1,075 characters, which sits comfortably in a cookie. Signed with ML-DSA-44 it is about 3,960. Signed with ML-DSA-65 it is about 5,145, and with ML-DSA-87 about 6,900.

So ML-DSA-44 technically fits, with about 137 bytes of headroom before you have even counted the cookie name, and everything above it does not. That “technically fits” is the dangerous case, because it survives your test environment and then fails in production the first time the app adds a claim mapper, or a refresh token, or a second cookie on the domain.

Cookie chunking, splitting one value across session.0, session.1 and so on, is the usual workaround, and it converts a hard failure into a different one. Every chunk goes back up in the Cookie request header on every request to the domain. That lands on server header limits instead, which is where 431 request header fields too large comes from. You have moved the failure from the browser to the reverse proxy.

What breaks besides cookies?

JWKS handling, and it breaks earlier than the algorithm switch does. ML-DSA public keys in JOSE use a new key type, AKP for algorithm key pair, rather than the familiar RSA, EC or OKP. It comes from the IETF COSE working group draft “ML-DSA for JOSE and COSE”, which is still an Internet-Draft, and Keycloak already carries it as KeyType.AKP with a pub member in AKPPublicJWK. A JWKS that advertises both a classical key and a PQC key is a perfectly valid document, and RFC 7517 section 5 is explicit that implementations should ignore JWKs with unknown key types inside a set rather than fail. A library that treats an unrecognized kty as a parse error is noncompliant, not merely brittle, and it will reject the whole set.

A mixed set is just two entries with different kty values:

{"keys": [
  {"kty": "RSA", "alg": "RS256",     "kid": "rsa-1", "n": "...", "e": "AQAB"},
  {"kty": "AKP", "alg": "ML-DSA-44", "kid": "pqc-1", "pub": "..."}
]}

That is the failure mode to test for deliberately, because the symptom does not point at the cause. Every login fails with a signature verification error, on tokens that are still signed with RS256, because the relying party never got as far as reading the RSA key. Publishing a dual-algorithm JWKS is a change you can make and roll back independently of flipping any signing algorithm, which makes it the right first experiment.

Order of operations therefore matters more than usual here. Publish the mixed JWKS first and watch for relying parties that break. Only then consider issuing anything signed with a PQC algorithm. Teams that do it the other way round debug two failures at once and cannot tell which client broke on size and which broke on key type.

Header limits are the third surface. An ML-DSA-87 signed access token in an Authorization header is around 7 KB before anything else in the request, and default header buffers in common proxies sit well below that. The token lifecycle questions do not change, but the cost of a fat token does.

Where is Keycloak’s post-quantum support actually at?

In progress and tracked in public, with the foundational pieces scoped to 26.8 and the broader readiness work to 27.0. Keycloak’s PQC readiness epic (keycloak/keycloak issue 43690, “Post-Quantum Cryptography (PQC) readiness”, opened 2025) sits on the 27.0 milestone with sixteen sub-issues, one of them complete at time of writing.

The one sub-issue already closed is the one this post is about. Issue 49865, “Milestone for cookies to be PQC ready”, covers Keycloak’s own browser cookies, and its children are about hash strength and symmetric key size rather than signatures: issue 49860 moves the AUTH_SESSION_ID_HASH and SESSION cookies off SHA-256 onto SHA-384 or SHA-512. That is the whole reason Keycloak’s own cookies are not in trouble here. They carry session identifiers and hashes, not asymmetrically signed tokens, so a signature format that grows tenfold does not touch them. The applications in front of Keycloak are where the problem lives.

The remaining sub-issues are more informative than the epic. Two of them, ML-DSA JCE algorithm mapping and ML-DSA key loading from Java keystores, are scoped to the 26.8 milestone. The keystore issue states the runtime constraint plainly: ML-DSA key loading “Requires JDK 24+ at runtime,” which follows JDK 24 shipping ML-DSA support in its standard crypto providers. The algorithm-mapping issue notes that JavaAlgorithm.getJavaAlgorithm() currently throws IllegalArgumentException for ML-DSA-44, 65 and 87, so keys can load and still not sign anything until that lands.

A related issue outside that epic, and with no milestone yet, is worth flagging to anyone building for financial-grade profiles. Keycloak issue 52819 records that FapiConstant.ALLOWED_ALGORITHMS contains only classical algorithms, and that SecureSigningAlgorithmExecutor.isSecureAlgorithm() “will actively reject ML-DSA even after providers exist.” The issue is explicitly gated on FAPI 2.0 formally including PQC algorithms. If you run FAPI client policies, PQC signing stays blocked by policy after it becomes technically possible, and that is deliberate rather than an oversight.

Passkeys are further out. Keycloak issue 50084, PQC support for WebAuthn and passkeys, carries a status/hold label and puts the realistic timeline at 2027 to 2028, blocked on upstream webauthn4j support and on any production browser or authenticator shipping ML-DSA. The identifier registrations are in place, with ML-DSA COSE identifiers allocated at IANA, while the browsers, the libraries and the authenticators are all still pending.

The transition design is also on the board. Keycloak issue 50304 covers PQ/T hybrid composite signatures for JOSE and COSE, following the IETF draft draft-ietf-jose-pq-composite-sigs, which pairs ML-DSA with ECDSA or EdDSA so a verifier that understands either component can still work during migration. Hybrids are larger again, which takes the size problem in the wrong direction and makes the cookie question more urgent, not less.

The epic states one non-goal that should shape everyone’s planning: PQC is not going to become the default, and classical algorithms are not being deprecated or removed. This is optional capability being added for operators who need to plan their own readiness.

What should you change now, before any of this ships?

Get tokens out of cookies. That is the whole prep, and it pays off whether or not you ever enable a PQC algorithm. A server-side session with an opaque session identifier in the cookie is a few dozen bytes on the wire and stays a few dozen bytes when the signature grows eighteen-fold. The trade-offs are laid out in cookies versus tokens versus server-side sessions, and post-quantum signatures move the balance decisively toward the third option.

The concrete checklist for the next two quarters:

  1. Find every place a signed token lands in a cookie. Backend-for-frontend implementations and server-rendered apps are the usual suspects, and anywhere you already chunk a cookie counts double.
  2. Measure your current tokens. Log the encoded length of issued ID and access tokens at p50 and p99. You need a baseline before you can reason about a 3 KB signature. Pasting one into the JWT token analyzer is the two-second version, and the backend verification guide covers where the length actually bites.
  3. Trim claims anyway. It will not save you on its own, but every byte of payload is a byte you are not spending on headroom. Group and role claims are usually the bloat, and client scopes versus roles is where you decide what belongs in a token at all.
  4. Raise proxy header buffers deliberately. Know your current limit rather than discovering it through a 431.
  5. Test a mixed JWKS against every relying party library you own. This is the cheap, reversible experiment that finds the brittle clients.
  6. Track the JDK. ML-DSA key loading in Keycloak needs JDK 24 or later at runtime, which in practice means JDK 25, the LTS that carries it. For most operators that is a matter of watching which JDK the official Keycloak image moves to rather than rebuilding a base image.
  7. Do not enable PQC signing in production yet. The algorithms are standardized, the library ecosystem is not, and nothing in the current threat model requires it this year.

Frequently asked questions

How big is an ML-DSA signature compared to RS256?

ML-DSA-44 is 2,420 bytes, ML-DSA-65 is 3,309 and ML-DSA-87 is 4,627, per FIPS 204. An RS256 signature over a 2,048-bit key is 256 bytes. That is 9.5x, 12.9x and 18.1x respectively, before base64url encoding adds roughly a third.

Only at the smallest parameter set, and with almost no margin. Browsers cap a cookie at about 4,096 bytes across the name and value. A stock ID token signed with ML-DSA-44 runs around 3,960 characters, ML-DSA-65 around 5,145 and ML-DSA-87 around 6,900. The last two cannot fit in a single cookie.

Does Keycloak support ML-DSA today?

Not yet for token signing. The work is tracked in the keycloak/keycloak repository under the PQC readiness epic on the 27.0 milestone, with ML-DSA keystore loading and JCE algorithm mapping scoped to 26.8. The FAPI client policy executor is separately tracked and still rejects ML-DSA.

What is the AKP key type in a JWKS?

AKP, for algorithm key pair, is the JOSE key type used for post-quantum keys including ML-DSA, alongside the existing RSA, EC and OKP types. It is defined in the IETF draft “ML-DSA for JOSE and COSE”. Client libraries that reject an entire JWKS on an unrecognized kty, rather than skipping that key, will fail on a mixed classical and PQC key set.

Should we switch to post-quantum signatures now?

No. Harvest-now-decrypt-later is a threat to key exchange, where recorded traffic can be decrypted later, not to signatures, which an attacker gains nothing from forging after the fact. Nothing currently requires the switch, the library ecosystem is incomplete, and the size consequences are real. Use the time to remove tokens from cookies, baseline your token sizes and test mixed JWKS handling, all of which are useful work independent of when PQC arrives.

Sources

The cluster, without the on-call

Skycloak runs real upstream Keycloak in the region you choose, and takes the upgrades, backups, certificate rotation and patching off your team. No fork, so you can export and self-host whenever you want.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman