---
title: "HMAC, hashing & HKDF — Crypto for Laravel | Roundly"
description: "HMAC webhook signatures, deterministic digests with an optional pepper, HKDF per-purpose subkeys and constant-time comparison."
url: https://roundly-consulting.com/open-source/docs/crypto-for-laravel/hmac-hashing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Crypto for Laravel](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)

# HMAC, hashing & HKDF

Hash\\Hmac signs and verifies keyed MACs — the building block for webhook signature checks. Secrets are passed per call and marked #\[\\SensitiveParameter\]; the class holds no key of its own:

```php
use RoundlyConsulting\Crypto\Facades\Crypto;
use RoundlyConsulting\Crypto\Hash\HashAlgorithm;

// GitHub-style "sha256=…" header check:
$expected = 'sha256='.Crypto::hmac(HashAlgorithm::Sha256)->signHex($request->getContent(), $webhookSecret);
$ok = Crypto::constantTimeEquals($expected, $request->header('X-Hub-Signature-256', ''));

// Deterministic digest → an indexable lookup column:
$lookupKey = Crypto::digest()->hex($emailLowercased);

// A per-purpose subkey from one root secret (HKDF):
$fingerprintKey = Crypto::hkdf()->derive($rootKey, 32, 'app/fingerprint');
```

Or call the classes the facade fronts directly:

```php
use RoundlyConsulting\Crypto\Hash\Hmac;
use RoundlyConsulting\Crypto\Hash\HashAlgorithm;

$hmac = new Hmac(HashAlgorithm::Sha256);

// GitHub-style "sha256=…" header check:
$expected = 'sha256='.$hmac->signHex($request->getContent(), $webhookSecret);
$ok = hash_equals($expected, $request->header('X-Hub-Signature-256', ''));

// Or verify raw signatures directly (constant-time):
$ok = $hmac->verify($payload, $signature, $webhookSecret);
```

| Method | Returns |
| --- | --- |
| `sign($message, $key)` | The raw-bytes HMAC. |
| `signHex($message, $key)` | The lower-case hex HMAC. |
| `verify($message, $signature, $key)` | bool — a constant-time check of a raw-bytes signature. |

## Deterministic digests

Hash\\Digest produces the same output for the same input — which is what makes an indexed equality lookup on hashed data possible, such as a blind index over an email or a token:

```php
use RoundlyConsulting\Crypto\Hash\Digest;

$digest = new Digest;                       // SHA-256

$lookupKey = $digest->hex($emailLowercased);          // deterministic → indexable column
$peppered  = $digest->withPepper($token, config('app.pepper')); // HMAC when a pepper is set
$raw       = $digest->raw($data);                     // raw bytes
```

withPepper() makes the plain-versus-keyed choice explicit: only null selects the plain digest. Any non-null pepper — even whitespace or an empty string — is used verbatim as the HMAC key, so a blank value can never silently downgrade a keyed digest. The data passed to raw(), hex() and withPepper() — tokens, passwords, personal data — is #\[\\SensitiveParameter\] too.

## HKDF — per-purpose subkeys

Hash\\Hkdf implements RFC 5869 extract-then-expand key derivation: one root secret, independent subkeys per purpose, so the key that seals values never also MACs a fingerprint:

```php
use RoundlyConsulting\Crypto\Facades\Crypto;
use RoundlyConsulting\Crypto\Hash\HashAlgorithm;

$fingerprintKey = Crypto::hkdf()->derive($rootKey, 32, 'app/fingerprint');       // no salt
$sealKey        = Crypto::hkdf(HashAlgorithm::Sha512)->derive($rootKey, 64, 'app/seal', 'app.hkdf/1');
```

Or call the class the facade fronts directly:

```php
use RoundlyConsulting\Crypto\Hash\Hkdf;

$subkey = (new Hkdf)->derive($rootKey, 32, 'app/fingerprint');   // SHA-256 by default
```

derive(string $ikm, int $length, string $info = '', string $salt = '') returns raw bytes:

- $ikm, the input keying material, is #\[\\SensitiveParameter\] and must not be empty.
- $length is required: 1 to 255 × the hash length (8160 bytes for SHA-256). PHP’s hash\_hkdf() reads 0 as “one hash length”; here 0 is refused, so a derived key’s size is never implicit.
- $info names the purpose. Separate its fields with a byte no field can contain, such as NUL.
- $salt is a non-secret value (RFC 5869 §3.1); empty means “not provided” — HashLen zero bytes.
- The output is byte-identical to hash\_hkdf(), pinned by the RFC 5869 Appendix A vectors (SHA-256 and SHA-1). SHA-1 stays available for interop only (HashAlgorithm::isLegacy()).
- An empty $ikm or an out-of-range $length throws Hash\\InvalidHkdfParameterException — a programming error, never a property of the data.

## Constant-time comparison

ConstantTime::equals() wraps hash\_equals so every secret, tag and MAC comparison routes through one place and never leaks, through timing, how much of a value matched. Both inputs are #\[\\SensitiveParameter\], as in PHP’s own hash\_equals(): the expected value is the secret, MAC, token or OTP itself, so neither ever reaches a stack trace:

```php
use RoundlyConsulting\Crypto\Hash\ConstantTime;

// $known is the trusted value, $user the attacker-influenced one:
$ok = ConstantTime::equals($knownExpected, $userSupplied);
```

## Hash algorithms

| Case | Value | Status |
| --- | --- | --- |
| `HashAlgorithm::Sha1` | sha1 | Legacy — collision-broken, interop only; isLegacy() returns true. |
| `HashAlgorithm::Sha256` | sha256 | Current (the default). |
| `HashAlgorithm::Sha384` | sha384 | Current. |
| `HashAlgorithm::Sha512` | sha512 | Current. |

OTP is separate: RFC 6238 mandates SHA-1 and authenticator apps default to it, so Otp\\OtpAlgorithm keeps SHA-1 first-class.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)
