0

I'm trying to build a generic function to encrypt HMAC values with a single global secret key, but that can be "scoped" or salted by application/uses. For instance, an HMAC for a session token signature should not be the same as the HMAC used for a password reset token signature.

Looking at, for example Django, they just do this by creating a secret as new_secret = sha256(salt + main_secret), basically, they salt the key by just concatenating and hashing, and use the result for the HMAC key. For me, this has the issue that if HMAC-SHA256 supports 64 bytes keys, the fact that you hash the secret reduces the effective key size to 32 bytes (due to the output of sha256).

Another implementation is AWS's signature v4 approach. They basically chain HMAC calls to produce new keys, for example, new_secret = HMAC(main_key, salt). In my humble opinion, this has the advantage that you never "lose bytes" from the secret, as long as you use a hash algorithm that outputs 64 bytes.

In my use case, I would have various parameters I would want to use to "salt" the key. One would be static values (e.g., an app/functionality identifier), and other values could be additional secret keys (e.g., user-provided keys).

Would something like this cause any issues?

def get_hmac_derived_key(secret, salts, digest=sha512):
    # We will use a similar key derivation and signature implementation
    # as AWS signature v4.

    def derive_key(key, msg):
        return hmac.new(key, msg, digest).digest()

    if not salts:
        raise ValueError("No salts provided.")

    derived = force_bytes(secret)

    for s in salts:
        derived = derive_key(derived, force_bytes(s))

    return derived

# Example usage
derived_secret = get_hmac_derived_key(BASE_SECRET, ['my.app.feature1', 'extra-salt-1', USER_SECRET])

signature = hmac.new(derived_secret, msg="my message", digestmod=sha256).digest()

Or do the multiple HMAC passes provide no benefit, and it should instead be just:

signature = hmac.new(sha512('my.app.feature1' + 'extra-salt-1' + BASE_SECRET + USER_SECRET), msg="my message", digestmod=sha256).digest()

Note: using sha512 for key derivation so the output is still 64 bytes (the block size of hmac-sha256, otherwise the python's HMAC code will just sha256(..) our key)

Lastly, the other alternative is to just include the salts/identifiers in the message, but that may give messages that overlap and may break the whole thing. E.g., signature = hmac.new(BASE_SECRET, msg='my.app.feature1' + ':' + 'extra-salt-1' + ':' + USER_SECRET + ':' + "my message", digestmod=sha256).digest()

1 Answer 1

1

The approach I would recommend here is HKDF. This is an HMAC-based key derivation function which is well suited to deriving keys in this way, and is in fact the algorithm used by TLS 1.3.

The process consists of two steps: extract and expand. The extract step can take salt, and the expand step can also take salt in its info parameter if you need to support additional salts. You can then derive whatever keys are needed. Note that if you use salt in the info parameter, make sure that your label and salt can't be confused (e.g., by inserting a separator that never appears in the label before the salt) to avoid attacks.

It is true that the extract step will reduce the security to 256 bits if you use HMAC-SHA-256. However, we already believe that 128 bits of security should be sufficient for the indefinite future, and 256 bits of security is believed to be far beyond the computational limits this planet can support using theoretical minimum energy. If you're really very concerned about this, you can use HMAC-SHA-512 to avoid reducing the security beyond the 64-byte key.

HKDF is also easy to implement if you have HMAC: all you need to do is perform one or two specially-formatted HMAC calls.

The approach of chaining derived keys with HMAC is likely secure. I've seen it used elsewhere, and I'm not aware of any problems with it. However, TLS 1.3 uses HKDF to perform repeated derivation for this purpose, and it is the standard approach, which I would recommend.

7
  • Looks like python doesn't have a built-in for HKDF, and getting an extra library just for this (if either AWS's or Django algorithms are good enough) is a bit of overkill. Could you comment the above two options too? Aug 3, 2022 at 22:14
  • Further, what I meant by "reduce the security to 256 bits", is actually not related to HMAC-SHA-256 output, but rather how the internal implementation of HMAC works. For example, HMAC-SHA-256 uses a block_size of 512 bits (64 bytes), so the key can be up to 64 bytes long. However, if you pass something greater, it is hashed to 32 bytes, basically reducing your original key's strength to half. Aug 3, 2022 at 22:16
  • HKDF is easily implementable with HMAC using the specification in the RFC. The algorithm is very simple. My comment on your approach is that it is probably secure, but it doesn't have a rigorous security proof, like HKDF, and you should use standard approaches when possible.
    – bk2204
    Aug 3, 2022 at 23:50
  • Both django and AWS also seem to have rolled their own implementation (either key concatenation or nested HMAC calls). Shouldn't that count as enough proof? Aug 4, 2022 at 1:09
  • Some popular frameworks also use MD5 or SHA-1 for various purposes. Just because major frameworks do it doesn't imply security. As I said, it's probably secure, but it's substantially better to use standard techniques, if only so when your code goes through an audit, you can simply say, "We use the standard technique," and save yourself immense hassle.
    – bk2204
    Aug 4, 2022 at 21:15

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .