HKDF Salt in Key Expansion

This weekend I made another addition to age-pkcs11, to follow best practices for HKDF key expansion from the shared secret at the core of the program. I’d been wanting to do this for a while, after reviewing some stuff I wrote about age and looking at the new V1 API there.

If you recall back in June when I went into detail on the X25519 cryptography in Age, near the end Age builds up a salt which, when combined with a label and supplied to the HKDF function ties the derived key to a specific context. This is a recommendation from the [Standards for Efficient Cryptography] (https://www.secg.org/) group, in SECV-1-V2 3.6.1, which calls it optional but which I gather is best practice. Age does it, and I wanted to follow suit.

Let me talk through what I think is happening here. First, the two parties use Elliptic Curve Diffe-Hellman to derive a shared secret. This shared secret isn’t the best choice for an encryption key, however. For one thing, we know that this must be a point on the curve used to perform ECDH. This doesn’t work well to provide the ideal cryptographic key, which should be so close to random data as to be indistinguishable from it. That’s why Age uses a key-derivation function, to turn that shared secret into a shared key. This is also the recommendation of SECV-1-V2.

So why the salt and the label? Why not simply pass through an HKDF without them? The explanation comes from RFC 5869, HMAC-based Extract-and-Expand Key Derivation Function (HKDF). For the salt, look at section 3.1:

HKDF is defined to operate with and without random salt. This is done to accommodate applications where a salt value is not available. We stress, however, that the use of salt adds significantly to the strength of HKDF, ensuring independence between different uses of the hash function, supporting “source-independent” extraction, and strengthening the analytical results that back the HKDF design.

Basically, it makes the HKDF stronger and ties this shared key to this specific use. If you didn’t use salt, some other use of this HKDF which happened to have the same input as the EC shared secret (say, using a random number for some purpose) would have the same key. Adding the salt ties the usage to this specific case.

Age’s method, which I follow, is to concatenate the public keys of both parties and use that as the salt. This binds it fairly well to the two parties.

The label explanation comes from section 3.2 of the RFC:

Its main objective is to bind the derived key material to application- and context-specific information. For example, ‘info’ may contain a protocol number, algorithm identifiers, user identities, etc. In particular, it may prevent the derivation of the same keying material for different contexts (when the same input key material (IKM) is used in such different contexts).

Age, and age-pkcs11, both use a protocol identifier as the info, age-encryption.org/v1/X25519 and age-encryption.org/v1/pkcs11 respectively. The value here is easier to understand if you think of what happens with Age V2 comes out. With different info strings, tied to different protocol versions, the same private keys turn into different shared keys. Different keys for different protocols.

Note that SECV-1-V2 doesn’t distinguish between the two, it just calls for an (optional) SharedInfo which, to quote, “…consists of some data shared by the entities intended to share the shared secret value Z.”

Bonus note: I’m not a cryptographer, I’m just trying to understand it better.