hash_hmac

Generates a keyed hash message authentication code using a shared secret and the chosen algorithm.

Signature
hash_hmac(string $algo, string $data, string $key, bool $binary = false): string
  • Added in PHP 5.1.2

Parameters

ParameterTypeDescription
$algostringName of the hashing algorithm, for example "sha256". Must be a cryptographic algorithm listed by hash_hmac_algos().
$datastringThe message to be authenticated. Binary safe.
$keystringThe shared secret key. Marked #[\SensitiveParameter] so it is redacted from stack traces.
$binaryoptionalbool= falseWhen 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.