HMAC and HKDF

What’s the right way to generate an encryption key from a secret? Well, with TLS 1.3, the right way is to use HKDF. So, let’s look at HMAC…

HMAC and HKDF

What’s the right way to generate an encryption key from a secret? Well, with TLS 1.3, the right way is to use HKDF. So, let’s look at HMAC and HKDF.

HMAC

HMAC (hash-based message authentication code) supports the usage of a key to hash data. This key is kept secret between Bob and Alice, and can be used to authentication both the data and that the sender still knows the secret. Overall HMAC can be used with a range of different hashing methods, such as MD5, SHA-1, SHA-256 (SHA-2) and SHA-3.

HMAC is a message authentication code (MAC) that can be used to verify the integrity and authentication of a message. It involves hashing the message with a secret key and thus differs from standard hashing, which is purely a one-way function. As with any MAC, it can be used with a standard hash function, such as MD5 or SHA-1, which results in methods such as HMAC-MD5 or HMAC-SHA-1. Also, as with any hashing function, the strength depends on the quality of the hashing function, and the resulting number of hash code bits. Along with this, the number of bits in the secret key is a factor in the strength of the hash. The figure below outlines the operation, where the message to be sent is converted with a secret key, and the hashing function, to an HMAC code. This is then sent with the message. On receipt, the receiver recalculates the HMAC code from the same secret key, and the message, and checks it against the received version. If they match, it validates both the sender and the message.

The outline code is [here]:

package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"io"
"os"
)
func getSalt(n int) []byte {
nonce := make([]byte, n)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
}
return (nonce)
}
func main() {
key:="000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"
m := "The quick brown fox jumps over the lazy dog"
argCount := len(os.Args[1:])
if argCount > 0 {
m = os.Args[1]
}
if argCount > 1 {
key = os.Args[2]
}
var secretKey, _ = hex.DecodeString(key)
message := []byte(m)
// salt := getSalt(16)
fmt.Printf("Message: %s\n", m)
fmt.Printf("Key: %x\n", secretKey)
// fmt.Printf("\nSalt: %x\n", salt)
hash := hmac.New(sha256.New, []byte(secretKey))
hash.Write(message)
// hash.Write(salt)
fmt.Printf("\nHMAC-Sha256: %x", hash.Sum(nil))
hash = hmac.New(sha512.New, []byte(secretKey))
hash.Write(message)
// hash.Write(salt)
fmt.Printf("\n\nHMAC-sha512: %x", hash.Sum(nil))
}

A sample run is [here]:

Message: The quick brown fox jumps over the lazy dog
Key: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
HMAC-Sha256: f87ad256151fc7b4c5dffa4adb3ebe911a8eeb8a8ebdee3c2a4a8e5f5ec02c32
HMAC-sha512: 0623d51f882717efa360aa2217d0b554b57ea018eb518178b23045941a6ae24450af5c980f6ebca94ca5314a8590991b4eab6daa3f0c109345433f44ee234d00

HKDF

HMAC Key Derivation function (HKDF) is used to derive an encryption key from a passphrase. Initially, HKDF creates a pseudorandom key (PRK) using a passphrase and a salt value (and any other random functions which are relavent), in order to produce an HMAC hash function (such as HMAC-SHA256), and along with a salt value. Next, the PRK output is used to produce a key of the required length. If we generate a 16-byte output (32 hex characters), we have a 128-bit key, and a 32-byte output (64 hex characters) will generate a 256-bit key. HKDF is used in TLS 1.3 for generating encryption keys [RFC 5869][article]. In this case, we will use SHA-256.

The following shows how an encryption key can be generated using HKDF, and where we need a secret and a salt value. Both Bob and Alice have the same secret and salt.

The outline code is [here]:

package main
import (
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"os"
"golang.org/x/crypto/hkdf"
)
func getSalt(n int) []byte {
nonce := make([]byte, n)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
}
return (nonce)
}
func main() {
hash := sha256.New
s := "The quick brown fox jumps over the lazy dog"
salt := getSalt(hash().Size())
info := []byte("")
argCount := len(os.Args[1:])
if argCount > 0 {
s = os.Args[1]
}
secret := []byte(s)
kdf := hkdf.New(hash, secret, salt, info)
key1 := make([]byte, 16)
_, _ = io.ReadFull(kdf, key1)
fmt.Printf("Secret: %s\n", s)
fmt.Printf("HKDF 16 byte key: %x\n", key1)
key2 := make([]byte, 32)
_, _ = io.ReadFull(kdf, key2)
fmt.Printf("HKDF 32 byte key: %x\n", key2)
}

A sample run is [here]:

Secret: The quick brown fox jumps over the lazy dog
Salt: c357e487d08ebb3a219d7370b26d424c7c642275e8a76cab1544d47b4c9458f4
HKDF 16 byte key: 2332a50d3306be40cfc044b0a48d5818
HKDF 32 byte key: 2b368335f306b6cf5ffd97b3077e0181de940d5e6c6eafd9b22dbeedf464c38a

Conclusions

And, so, in TLS 1.3, you should use HKDF to generate an encryption key.