Signature
hash_hmac(
string $algo,
string $data,
#[\SensitiveParameter] string $key,
bool $binary = false
): stringPurpose
hash_hmac() produces a keyed hash of a message using the HMAC method. A plain hash such as hash('sha256', $data) can be computed by anyone who sees the data. An HMAC also mixes in a shared secret key, so only parties who know that key can produce or check the digest. Typical uses are signing a webhook payload, protecting a cookie value against tampering, or authenticating a URL parameter set.
An HMAC proves integrity and authenticity relative to the key. It does not encrypt anything: the message itself stays readable.
Signature
Parameters and defaults
$algo: the name of a hashing algorithm, for example"sha256". The names accepted here are listed byhash_hmac_algos(). Non-cryptographic algorithms are not allowed.$data: the message to authenticate, as a string.$key: the shared secret key. The#[\SensitiveParameter]attribute keeps its value out of stack traces.$binary: defaults tofalse, which returns lowercase hexadecimal characters. Passtrueto receive the raw binary digest.
Return value and failure cases
The function returns a string. With the default $binary = false it is a lowercase hex string whose length is twice the digest size in bytes: 64 characters for SHA-256, 128 for SHA-512. With $binary = true it is the raw digest bytes, such as 32 bytes for SHA-256.
Since PHP 8.0 an unknown algorithm or a non-cryptographic one (such as crc32) throws a ValueError. Before PHP 8.0 the function emitted a warning and returned false. The non-cryptographic algorithms were already disabled for HMAC in PHP 7.2.
Example 1: the basic call
The same inputs always give the same digest, so the output below is stable.
echo hash_hmac('sha256', 'The quick brown fox jumped over the lazy dog.', 'secret'), "\n";9c5c42422b03f0ee32949920649445e417b2c634050833c5165704b825c2a53b
Example 2: output formats and lengths
This example compares hex and binary output, several algorithms, and the effect of changing the key. Only lengths and booleans are printed, which keeps the focus on the shape of the result.
$data = 'order=1001';
$key = 'k';
$hex = hash_hmac('sha256', $data, $key);
$bin = hash_hmac('sha256', $data, $key, true);
echo json_encode([
'sha256Hex' => strlen($hex),
'sha256Binary' => strlen($bin),
'binaryMatchesHex' => bin2hex($bin) === $hex,
'sha512Hex' => strlen(hash_hmac('sha512', $data, $key)),
'sha1Hex' => strlen(hash_hmac('sha1', $data, $key)),
'differentKey' => hash_hmac('sha256', $data, 'K') !== $hex,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"sha256Hex":64,"sha256Binary":32,"binaryMatchesHex":true,"sha512Hex":128,"sha1Hex":40,"differentKey":true}
Example 3: signing, verifying and a rejected algorithm
A verifier recomputes the HMAC from the received message and compares it with the supplied signature. Use hash_equals() for the comparison so it runs in constant time with respect to the contents.
function sign(string $message, string $key): string {
return hash_hmac('sha256', $message, $key);
}
$key = 'server-secret';
$message = 'user=42&plan=basic';
$signature = sign($message, $key);
$result = [
'valid' => hash_equals($signature, sign($message, $key)),
'tampered' => hash_equals($signature, sign('user=42&plan=admin', $key)),
'wrongKey' => hash_equals($signature, sign($message, 'other-key')),
];
try {
hash_hmac('crc32', $message, $key);
$result['crc32'] = 'accepted';
} catch (ValueError $e) {
$result['crc32'] = 'rejected';
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"valid":true,"tampered":false,"wrongKey":false,"crc32":"rejected"}
Edge cases
- The result depends on the exact bytes of
$data. A trailing newline, different encoding or reordered query parameters give a completely different digest, so both sides must agree on one canonical message format. - Hex output is always lowercase. If you receive an uppercase signature, normalize it before comparing, or compare decoded bytes.
- Binary output may contain null bytes and non-printable characters. Encode it with
bin2hex()orbase64_encode()before logging, putting it in JSON or placing it in a URL. - Algorithm names are chosen by your code. If a client can pick the algorithm, validate it against an allow-list first, because an unexpected name throws a
ValueError.
Common mistakes
- Comparing with
==or===. Ordinary string comparison can leak timing information. Usehash_equals($known, $userSupplied)with the trusted, locally computed value first. - Swapping the argument order. The key is the third argument, after the data. Passing them the wrong way round still returns a digest, but it will not match what other systems compute.
- Using a weak or hard-coded key. An HMAC is only as secret as its key. Generate a long random key, store it outside the source code and rotate it when needed.
- Treating an HMAC as a password hash. It is fast by design. For storing passwords use
password_hash()instead. - Assuming it hides the data. The message must still be protected separately if it is confidential.
- Checking for
falseon failure. Modern PHP throwsValueError; it does not returnfalsefor a bad algorithm.
Related functions
hash_hmac_algos()lists the algorithm names suitable for HMAC.hash_hmac_file()computes an HMAC over the contents of a file.hash_equals()compares two strings in a timing-safe way.hash()computes an unkeyed digest.
Recap
hash_hmac($algo, $data, $key, $binary) returns a keyed digest, hexadecimal by default and raw bytes when $binary is true. It throws ValueError for unknown or non-cryptographic algorithms. Keep the key secret, canonicalize the message, and verify signatures with hash_equals().
Tested examples
echo hash_hmac('sha256', 'The quick brown fox jumped over the lazy dog.', 'secret'), "\n";9c5c42422b03f0ee32949920649445e417b2c634050833c5165704b825c2a53b
$data = 'order=1001';
$key = 'k';
$hex = hash_hmac('sha256', $data, $key);
$bin = hash_hmac('sha256', $data, $key, true);
echo json_encode([
'sha256Hex' => strlen($hex),
'sha256Binary' => strlen($bin),
'binaryMatchesHex' => bin2hex($bin) === $hex,
'sha512Hex' => strlen(hash_hmac('sha512', $data, $key)),
'sha1Hex' => strlen(hash_hmac('sha1', $data, $key)),
'differentKey' => hash_hmac('sha256', $data, 'K') !== $hex,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"sha256Hex":64,"sha256Binary":32,"binaryMatchesHex":true,"sha512Hex":128,"sha1Hex":40,"differentKey":true}
function sign(string $message, string $key): string {
return hash_hmac('sha256', $message, $key);
}
$key = 'server-secret';
$message = 'user=42&plan=basic';
$signature = sign($message, $key);
$result = [
'valid' => hash_equals($signature, sign($message, $key)),
'tampered' => hash_equals($signature, sign('user=42&plan=admin', $key)),
'wrongKey' => hash_equals($signature, sign($message, 'other-key')),
];
try {
hash_hmac('crc32', $message, $key);
$result['crc32'] = 'accepted';
} catch (ValueError $e) {
$result['crc32'] = 'rejected';
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"valid":true,"tampered":false,"wrongKey":false,"crc32":"rejected"}