All writing

2 min read

Crypto-agility in three pull requests

What I learned turning post-quantum scan findings into changes that maintainers of public-sector code can actually merge.

  • Post-quantum
  • Security
  • Open source

A finding is not a fix

Working on PQNavigator, I see a lot of scan output. "RSA used here", "ECDSA used there". It is all correct and almost none of it is useful on its own. A maintainer who gets a list of fifty quantum-vulnerable call sites will close the tab.

So I picked three findings in open-source projects from the Dutch public sector and tried to turn each one into a pull request that is safe to merge today. Each one taught me a different pattern.

1. Stop hardcoding the algorithm

In a proxy from the Ministry of VWS, the JWT signing algorithm was a class constant. That is the opposite of crypto-agility: changing it means a code change, a review and a release.

python minvws/nl-mgo-dvp-proxy#8
class ClientAssertionJwtIssuer:-    JWT_ALG: str = "RS256"+    KEY_STORE_FALLBACK_PRIVATE_KEY_ID: str = (+        "client_assertion_jwt_fallback_private_key"+    )

The fix reads the algorithm from config, defaults to ML-DSA-65 and falls back to RS256 when no post-quantum key exists. Nothing changes in production until someone adds the new key.

python
try:    self.__private_key = self.__jwk_repository.get_first_key_from_store(        self.KEY_STORE_PRIVATE_KEY_ID    )except KeyError:    self.__using_fallback_alg = True    self.__private_key = self.__jwk_repository.get_first_key_from_store(        self.KEY_STORE_FALLBACK_PRIVATE_KEY_ID    )

2. When you cannot switch, shorten the window

The Fundament Kubernetes operator creates an RSA-4096 CA that is valid for ten years. The honest answer is that it cannot move to ML-DSA yet: cert-manager only supports RSA, ECDSA and Ed25519.

But you can still reduce the risk. Mosca’s inequality says you are in trouble when the time data must stay secure (X) plus the time to migrate (Y) is longer than the time until a quantum computer arrives (Z). A ten-year CA makes X huge. Three years, with renewal 90 days before expiry, makes it small, and forces a new decision point before anyone commits to RSA again.

go fundament-oss/fundament#424
"privateKey":  map[string]any{"algorithm": "RSA", "size": int64(4096)},"duration":    "26280h", // 3 years"renewBefore": "2160h",  // 90 days

3. Protect what attackers are recording today

The City of Amsterdam runs a proxy to the national population register. Citizen service numbers stay sensitive for a lifetime, which makes them the textbook target of "harvest now, decrypt later".

The change adds hybrid post-quantum TLS (X25519MLKEM768) on the outbound connection, behind a flag, and checks that the linked OpenSSL is new enough. If it is not, it logs a warning and uses classical TLS instead of failing.

python Amsterdam/brp-amsterdam-api#195
if ssl.OPENSSL_VERSION_INFO[:2] < _MIN_OPENSSL_FOR_PQC:    logger.warning(        "PQC TLS requested (BRP_ENABLE_PQC_TLS) but linked OpenSSL %s is older "        "than 3.5 and does not support the ML-KEM hybrid TLS 1.3 group - "        "falling back to classical TLS.",

What the three have in common

  • Every change is opt-in or has a fallback. Merging it changes nothing until the team decides.
  • Every change explains itself in the code, with a link to the reasoning, so the next developer does not undo it.
  • Every change is tested for real. The TLS change comes with a loopback handshake test, not just a config check.

Post-quantum migration will take years. It will not happen in one big-bang project, but in hundreds of small changes like these. The sooner they become boring, the better.

joey.oosenbrug@gmail.com