Welcome back to Cryptography Dispatches, my lightly edited newsletter on cryptography engineering. As usual, there's no tracking, so reply and let me know your thoughts, what you'd like to hear about, or just that you're reading.
— Filippo
When talking about high-level application cryptography APIs I usually hear mentioned libsodium, Tink, pyca/cryptography, and NaCl.
One of these things is not like the others! The value NaCl had 10 years ago was that it was an opinionated library at a time when all cryptography libraries were choose-your-own-adventure toolkits, but its APIs are not high-level, and even its constructions are unsafe by today’s standards.
NaCl refers to a set of APIs implemented in C by an old library published at nacl.cr.yp.to. No one really uses the original library, partially because it was a pain to build and package, but two of its constructions got ported to a number of languages, including Go: box and secretbox.
secretbox is for encrypting a message with a symmetric key, and it looks like this in Go.
func Seal(out, message []byte, nonce *[24]byte, key *[32]byte) []byte
func Open(out, box []byte, nonce *[24]byte, key *[32]byte) ([]byte, bool)
It’s nothing else than XSalsa20Poly1305, and you could use any other AEAD with large nonces like straight XChaCha20Poly1305 in the exact same way. (It also lacks the convenient ChaCha20Poly1305 twist of skipping the leftovers of the first ChaCha20 block after generating the Poly1305 key, so the ciphertext starts on a block boundary.)
It’s not even a good high-level AEAD API, because it leaves nonce management to the application, when it should just generate it randomly (192 bits is enough to do so safely) and prepend it to the ciphertext. The Go AEAD interface does this wrong, too, and I have seen so many developers struggling with where to store the nonce. (The answer is to prepend it to the ciphertext.) Some protocols do require controlling nonces (the TLS record layer, for example), for good reason, but they should be served by a lower-level API than what is provided to applications. secretbox was a missing opportunity here.
box is for encrypting a message with asymmetric keys, and it looks like this in Go.
func Seal(out, message []byte, nonce *[24]byte, peersPublicKey, privateKey *[32]byte) []byte
func Open(out, box []byte, nonce *[24]byte, peersPublicKey, privateKey *[32]byte) ([]byte, bool)
The first thing you might notice, after the fact that again we’re asking the application for a nonce we could have randomized for them, is that sealing a message also requires a private key and opening one also requires a public key. That’s because box is static Diffie-Hellman between two long-term key pairs: the sender and the receiver. Having a stable sending key is uncommon enough that libsodium introduced an anonymous variant which generates an ephemeral sending key pair for each message, and long-term secrets should be avoided rather than encouraged.
The presence of a sending key might make you think the message is signed by it, but it’s not. box provides only authentication, meaning that the recipient can change the message, too, and it will look the same as if the sender sent it. This is supposed to provide repudiability, a property I never really saw the value of.
Worse, there is actually nothing to distinguish a sender→recipient box from a recipient→sender one. Any third-party can take a box you sent and reflect it back to you, and it will look like it came from the recipient.
For example, if you have a protocol where two parties exchange boxes every hour to confirm everything is fine, like this…
A → B: box(pubB, privA, "One o'clock and all is well")
B → A: box(pubA, privB, "One o'clock and all is well")
… then a MitM can just take the “A → B” message and send it back to A, even if B was captured by a bear and a fox, and it will look fine to A.
The original NaCl docs suggest a scheme to protect against this, even if they don’t mention the attack and focus more on avoiding nonce reuse, which again would not be a problem if they were randomized.
the lexicographically smaller public key can use nonce 1 for its first message to the other key, nonce 3 for its second message, nonce 5 for its third message, etc., while the lexicographically larger public key uses nonce 2 for its first message to the other key, nonce 4 for its second message, nonce 6 for its third message, etc
Note that for this trick to protect you against reflection attacks, you have to not only use it as your nonce generation scheme, but also to verify all the incoming nonces.
Now, I don’t know about you, but I wouldn’t call this a high-level API.
I mostly focused on the construction here because again no one uses the library itself from 2008–2011, but I feel like the C API still deserves a honorable mention, because… excuse me, what!?
crypto_box()
takes a pointer to 32 bytes before the message, and stores the ciphertext 16 bytes after the destination pointer, the first 16 bytes being overwritten with zeros. crypto_box_open()
takes a pointer to 16 bytes before the ciphertext and stores the message 32 bytes after the destination pointer, overwriting the first 32 bytes with zeros.
(The quote above is from the libsodium docs because the NaCl ones were even too hard to quote.)
Those prefix bytes apparently must be zeroes or the behavior is undefined, they must be counted in the length parameters, and the two different constants are conveniently named crypto_box_BOXZEROBYTES
and crypto_box_ZEROBYTES
.
libsodium added replacement _easy
APIs, and mentions in its docs that “the original NaCl crypto_box API is also supported, albeit not recommended”. Well, yeah.
Follow-ups
In Dispatch #4, I mention that OpenSSH encrypts the FIDO2 key handle when you encrypt a security key-backed SSH key, and I was not sure if you could actually rely on it being necessary to use the key. tialaramex on HN pointed out that WebAuthN does guarantee that: the key handle has to either include at least 100 bits of entropy, or be the encrypted key material.
There’s a wrinkle in that OpenSSH implements FIDO2, not WebAuthN, but most hardware tokens implement both. I appreciated this line, which means my shtick is getting across.
You’d presumably hate both specifications, because they drag in (and rely upon) registries for a bunch of technically unnecessary stuff and not even for the practical engineering reason I’ve excused in my sister posts to this thread.
Links
Finally, I really need you all to read this thread about the festering misogyny in InfoSec, and reflect on how we all enable it by maintaining professional relationships with the people who harass our colleagues and drive them out of the industry.
A picture
Harriman State Park is pretty great.