Welcome back to Cryptography Dispatches, my lightly edited newsletter on cryptography engineering. This was going to be a Twitter thread, but then I remembered it's what this newsletter is for. Please do reply, like you would on Twitter.
Cryptographic protocols and specifications often come with registries that map numeric or string identifiers to algorithms or suites.
Something like this.
You’ll find them everywhere. TLS, X.509, SSH, PGP, you name it. They enumerate signature algorithms, hash functions, ciphers, key exchanges, encodings… all sorts of primitives and parameters. There is even a whole bureaucracy set up to handle the IETF/IANA ones. People have opinions on its bylaws.
I think these registries are a design smell at best, and outright harmful in most designs. What they encourage is designing for cryptographic agility: a “gotta catch ‘em all” approach to cryptographic primitive choices which we now know is a very common source of protocol vulnerabilities. If you need to enumerate the options you support, it means that not only you support multiple ones, which is already bad, but you need to communicate the choices in the protocol itself, meaning you do runtime negotiation. Do you want bugs? Because that’s how you get bugs.
Even worse than protocol registries are abstract registries, like the IANA registry of AEADs. They imply that if you are going to use an AEAD in your protocol you should make it parametrizable, and they conveniently already enumerated all the options for you, in case you wanted to defer the choice between
What’s the alternative? The alternative is to have one joint, and keep it well-oiled (to quote Adam Langley from the cryptographic agility essay linked above). Instead of parametrizing every choice, each version of a protocol or a format should pick one specific primitive, and if anything needs to change the protocol or format version can be bumped.
For example, age v1 uses ChaCha20-Poly1305, HKDF-SHA256, and HMAC-SHA256. There is no identifier next to the ciphertexts or MACs, there is just a format version number in the header. If we ever need to change one of the primitives, we’ll make age v2. Old tools would not have supported the new primitives anyway, and new tools can still support both versions if it’s safe to do so. The industry now understands updating and patching software quickly is critical, so the case for runtime configuration-based mitigations is weaker than ever.
If you do need to support multiple options in a format (which I mostly find objectionable but hey, not a perfect world, I get it), for example to support different key types, you should still not need registries: the type of a signature for example should be a property of the key being used to verify it. (This mantra I got from Sophie Schmieg.) You might need a name for the key type if you load it from disk, but not for the signature. Otherwise, you end up with JWT, where the signature gets to tell the verifier to use an RSA public key as an HMAC secret key.
This is not to say that interchangeable primitives that implement a well-defined API, like AEADs or cough prime-order groups, are bad! Quite the contrary, we do need to have a variety of interchangeable well-studied primitives available off-the-shelf, so that protocols can compose them easily and safely, and even quickly replace a broken one (in a new version) if something were to happen. However, a certain protocol version should be instantiated with a specific set of concrete primitives, which make the registry unnecessary.
For example, a file exchange protocol version could instantiate a specific PAKE with a specific prime-order group, and pick CPace-ristretto255 for password authentication.
Whether libraries should implement instantiations (like CPace-ristretto255) or take an interface (like CPace over any prime order group) is a more nuanced issue, but by the time the bytes hit the wire, there must be no choice left to communicate, so there is no need for registries.
Admittedly, age does have something resembling a registry: the recipient types, like
scrypt. Recipient types however have different user-visible behaviors, they aren’t just internal cryptographic choices. Maybe better names for them would have been
passphrase, respectively. (They are also the main and only extensibility joint in the format: anyone can implement a custom recipient type to support hardware tokens, or KMS, or their key distribution system. Extension registries are not what I’m writing about.)
In general, that’s how I feel about options in cryptographic tools and formats: if the choice leads to different semantics, like passphrase vs. public key encryption, it might be legitimate to let the user choose; if the choice has no semantic implications, like between hash functions or between ciphers, it’s our job as cryptography engineers to make it, even if it’s not always an obvious one. The user—or the developer—would be in a worse, not better, position to make it.
Since this round I’m going to get roasted for my opinions, might as well throw my movie tastes in there as well. Here’s the wall in front of my TV that I recently decorated with my favorite movies’ posters. (Note the server now besides the sofa, and the cute zebra shark.)