Security Tip: Use HMAC Hashes To Verify Data
[Tip#66] For those situations where you need to generate a repeatable hash or signature, reach for HMAC, rather than MD5 or SHA1.
Let’s pick up from last week’s Security Tip: Do You Really Need a Hash for That? (Go read that first, if you haven’t yet!)
We’ve talked about situations where you don’t need to reach for hashes, but what if you do need a hash?
There are a few reasons why you’d want to hash some data, and today we’re going to cover the use case of verifiable signatures or repeatable hashes. These are hashes generated from a known piece of data, and then compared to another hash generated from a different piece of data. If the hashes match, then the data is the same and unmodified.
The simplest example of this are Laravel’s Signed URLs. These work by generating the full URL, hashing that URL (using HMAC), and adding the hash signature onto the URL. When the user uses the URL, the application extracts the signature, hashes the provided URL and checks if the signature matches the new hash. If they match, the URL hasn’t been modified and can be trusted. If they don’t match, the URL cannot be trusted.
This sounds great, but how do we generate a hash like this? Also, how can we use this between different apps to protect things like API payloads? Using a raw hashing algorithm won’t prevent someone else from generating a hash of the modified data. Instead, we need to use a shared secret key known only to the servers which are generating and verifying the signatures, and then use this as part of generating our hash.
While you could just add a shared secret into the plaintext value you’re hashing (please don’t do this), a secure method is to use a Hash-based Message Authentication Code (HMAC) with PHP’s hash_hmac()
function:
hash_hmac(
string $algo,
string $data,
string $key,
bool $binary = false
): string
We can use it like this:
> $algo = 'sha256';
> $plaintext = 'One ring to rule them all';
> $secretKey = 'secret';
> hash_hmac($algo, $plaintext, $secretKey);
= "f1ae7cb307846761e8c42d2b12c8c400328e8fd692996c5fb65e722e703d3825"
An attacker can control both the $plaintext
and hash output, but without knowing the $secretKey
, they cannot generate a hash that matches the modified $plaintext
.
If you’re generating hashes within your application only, you could use config('app.key');
directly as the HMAC key, but I would recommend generating and using a separate key for this. This allows you to rotate keys independently, as well as share keys if needed.
As you’d expect, HMAC signatures are often used in APIs and especially webhooks, to prevent tampering and forging values. If you can verify the signature, you can trust the API & webhook payloads. Many API SDKs handle this component automatically for you.
If you’ve used HMACs or other forms of signatures and repeatable hashes before, I’d love to hear what you’re doing with them. Leave a comment below with the details!