Cryptographic protocols and specifications often come with registries that map numeric or string identifiers to algorithms or suites.
Something like this.
1 RSA-PSS-SHA256 2 RSA-PSS-SHA512 3 ECDSA-P256-SHA256 4 ECDSA-P521-SHA512 5 Ed25519 ...
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 AEAD_AES_256_GCM_SIV
, AEAD_CHACHA20_POLY1305
, and AEAD_AES_128_CCM_SHORT
.
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 X25519
and scrypt
. Recipient types however have different user-visible behaviors, they aren’t just internal cryptographic choices. Maybe better names for them would have been publickey
and passphrase
, respectively.1 (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.)
It’s interesting to think how “make it a property of the key” would work here. Maybe each recipient stanza would be an opaque block and it would be passed to all the configured identities (private keys)? Type confusion is less worrying in age than JWT because the wrong identity will just not produce the right file key, but there might be a CCA angle to it. Maybe v2 should do away with recipient type names. ↩