Hashing7 min readExamples run on PHP 8.5.10

hash_hmac()

Learn how PHP 8.4 hash_hmac() creates keyed message digests, what its parameters and errors mean, and how to verify signatures safely.

Signature

hash_hmac(
    string $algo,
    string $data,
    #[\SensitiveParameter] string $key,
    bool $binary = false
): string
Available since PHP 5.1.27.48.08.18.28.38.48.5All versions →

Purpose

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 by hash_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 to false, which returns lowercase hexadecimal characters. Pass true to 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.

Example
<?php
echo hash_hmac('sha256', 'The quick brown fox jumped over the lazy dog.', 'secret'), "\n";
Output
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.

Example
<?php
$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";
Output
{"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.

Example
<?php
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";
Output
{"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() or base64_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

  1. Comparing with == or ===. Ordinary string comparison can leak timing information. Use hash_equals($known, $userSupplied) with the trusted, locally computed value first.
  2. 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.
  3. 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.
  4. Treating an HMAC as a password hash. It is fast by design. For storing passwords use password_hash() instead.
  5. Assuming it hides the data. The message must still be protected separately if it is confidential.
  6. Checking for false on failure. Modern PHP throws ValueError; it does not return false for a bad algorithm.
  • 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

Basic HMAC-SHA256 digest
<?php
echo hash_hmac('sha256', 'The quick brown fox jumped over the lazy dog.', 'secret'), "\n";
Output
9c5c42422b03f0ee32949920649445e417b2c634050833c5165704b825c2a53b
Hex versus binary output and digest lengths
<?php
$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";
Output
{"sha256Hex":64,"sha256Binary":32,"binaryMatchesHex":true,"sha512Hex":128,"sha1Hex":40,"differentKey":true}
Signing, verifying and a rejected algorithm
<?php
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";
Output
{"valid":true,"tampered":false,"wrongKey":false,"crc32":"rejected"}