hash_hmac
Generates a keyed hash message authentication code using a shared secret and the chosen algorithm.
hash_hmac(string $algo, string $data, string $key, bool $binary = false): string- Added in PHP 5.1.2
Parameters
| Parameter | Type | Description |
|---|---|---|
$algo | string | Name of the hashing algorithm, for example "sha256". Must be a cryptographic algorithm listed by hash_hmac_algos(). |
$data | string | The message to be authenticated. Binary safe. |
$key | string | The shared secret key. Marked #[\SensitiveParameter] so it is redacted from stack traces. |
$binaryoptional | bool= false | When true, outputs raw binary data instead of lowercase hexits. |
Return value
stringThe HMAC as lowercase hexits, or the raw binary HMAC when binary is true.
Errors
Throws a ValueError if algo is unknown or is a non-cryptographic hash function.
Changelog
- PHP 8.0.0Now throws a ValueError if algo is unknown or is a non-cryptographic hash function; previously false was returned with an E_WARNING.
- PHP 7.2.0Use of non-cryptographic hash functions (adler32, crc32, crc32b, fnv132, fnv1a32, fnv164, fnv1a64, joaat) was disabled.
hash_hmac() combines a message with a secret key using the HMAC construction from RFC 2104. Anyone holding the key can compute the same value; anyone without it cannot forge one, even with unlimited sample messages and signatures.
When to reach for it
Use it whenever you need to prove that a message came from someone holding a shared secret and was not modified in transit: verifying Stripe or GitHub webhook signatures, signing your own API requests, sealing a cookie or a URL so it cannot be edited, and building the signature half of a JWT with HS256.
What trips people up
The most common mistake is rolling your own with hash('sha256', $secret . $data). That construction is vulnerable to length-extension attacks on SHA-256 — an attacker can append data and produce a valid digest without knowing the key. HMAC exists precisely to close that hole; do not substitute it.
The second is verifying with ===. Always compare with hash_equals() so a wrong signature does not leak timing information.
Third, when a provider's docs show a hex signature, remember the default here is hex. If a provider expects base64 you need base64_encode(hash_hmac($algo, $data, $key, true)) — base64 of the hex string is a different value and will never match.
Example
When to use it
- Verify a GitHub or Stripe webhook signature header before trusting the payload.
- Sign a download URL so its expiry timestamp cannot be edited by the recipient.
- Produce the signature segment of an HS256 JSON Web Token.
Frequently asked questions
What is the difference between hash and hash_hmac?
hash() proves only that data was not accidentally corrupted; hash_hmac() also proves it came from someone holding the secret key. Use hash_hmac() for anything an attacker might want to forge.
Why does hash_hmac reject crc32b?
PHP 7.2 disabled non-cryptographic algorithms for HMAC, and PHP 8.0 turned that into a ValueError. Only algorithms listed by hash_hmac_algos() are accepted.
Why does my HMAC never match the provider's signature?
Almost always an encoding or body mismatch — check whether they expect base64 (pass true as the fourth argument first) rather than hex, and sign the exact raw request body rather than a re-encoded version.
More examples
Verifying a webhook signature
Only the untouched body with the correct secret verifies. Note that the raw body string must be used exactly as received — decoding and re-encoding the JSON would change whitespace and break the signature.
<?php
$secret = 'whsec_demo';
$body = '{"id":"evt_1","type":"invoice.paid"}';
$header = 'sha256=' . hash_hmac('sha256', $body, $secret);
function verify(string $body, string $header, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
return hash_equals($expected, $header);
}
var_dump(verify($body, $header, $secret));
var_dump(verify($body . ' ', $header, $secret)); // body tampered
var_dump(verify($body, $header, 'wrong-secret'));A signed, expiring URL
The signature covers both the path and the expiry, so editing either invalidates it. The false result shows that without the key an attacker cannot produce a signature for the extended expiry.
<?php
$key = 'url-signing-key';
$path = '/files/report.pdf';
$exp = 1893456000;
$sig = hash_hmac('sha256', "$path|$exp", $key);
$url = "$path?expires=$exp&sig=$sig";
echo $url, PHP_EOL;
// attacker pushes the expiry out but cannot resign
$tampered = 9999999999;
var_dump(hash_equals($sig, hash_hmac('sha256', "$path|$tampered", $key)));
Discussion