Nonce handling in CryptoKit’s HPKE Sender & Recipient

G'day all,

I'm working through the creation of a cross-platform decryption implementation for CryptoKit's HPKE and wish to use the Sender & Recipient type.

I have been able to engineer the derived key, but the missing link is the nonce that is created and utilised by HPKE.Sender.seal(). I understand that I could create the key exchange and sealed box by myself and set my own random nonce, but I want to be able to utilise the HPKE.Sender.seal() functions to assist with this as well as create ciphertext data externally that can be opened with HPKE.Recipient.open().

By looking at Apple's open-source code available here, I can see that it seems to be exporting a key based on a "base_nonce" label on the context, which I think is what HPKE.Sender's exportSecret(context:outputByteCount:) can achieve.

However using senders exportSecret(context:outputByteCount:) in the following way:

let noncedata = try hpkeSender.exportSecret(context: Data("base_nonce".utf8), outputByteCount: 12)

even just for one message (so the sequence number would be 0 and thus this data block unchanged), the AES-GCM implementation still returns a "cipher: message authentication failed" error. This is specifically in Go, but can be replicated in Python easily. I'm confident that the derived key is correct and is being fed to AES-GCM with the ciphertext correctly, and it's just the nonce generation that is not understood.

Replies

I’d like to clarify your goals here. It seems like you’re trying to use Apple CryptoKit to encrypt messages with HPKE, send those messages to a non-Apple platform, and then use your own implementation of HPKE to decrypt them. Is that right?

If so, I’m not sure why you’re asking about getting the base_nonce value on the Apple CryptoKit side. That value isn’t a random number, but rather it’s derived using the KDF from the key schedule inputs. The receiver is expected to derive it in the same way.

Check out this snippet from RFC 9180:

base_nonce = LabeledExpand(secret, "base_nonce",
                           key_schedule_context, Nn)

Note how this is in the KeySchedule type, which is generic in ROLE, that is, the S or R for the sender or receiver. So, both peers must set up this key schedule object, which is why they agree on the base_nonce value.

Also, LabeledExpand is interesting. It’s basically wrapper around the KDF’s expand operation which takes various inputs, assembles them in a specific way, and passes that as the input to the core KDF expand operation. That expand operation does the usual KDF thing, that is, it derives the specified number of seemingly random bytes from the input.

Or at least that’s my reading of the RFC (-: I don’t have a lot of deep experience with HPKE.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"