Skip to content
ecdsa.com

HMAC-SHA256 sign/verify · OpenSSL

How to sign and verify a message with HMAC-SHA256 with OpenSSL

To sign and verify a message with HMAC-SHA256 with OpenSSL, run openssl dgst -sha256 -hmac <secret> over the exact bytes and compare the hex it prints with the received value; OpenSSL 3 also has openssl mac for the same computation. This recipe reproduces GitHub's documented webhook example from the shell — handy for checking a captured delivery by hand.

Tested with OpenSSL 3.6.4 (Debian forky (buildpack-deps:forky-curl)).

compute the X-Hub-Signature-256 header for a body
# The body exactly as sent: printf adds no newline (echo would, and change the MAC)
printf '%s' 'Hello, World!' > body.bin
# HMAC-SHA256 keyed with the shared secret; -r prints "<hex> *<file>"
mac=$(openssl dgst -sha256 -hmac "It's a Secret to Everybody" -r body.bin | cut -d' ' -f1)
echo "X-Hub-Signature-256: sha256=$mac"
check a captured delivery against the header it came with
# Check a captured delivery: its raw body (body.bin) and the header it came with
header='sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17'
expected="sha256=$(openssl dgst -sha256 -hmac "It's a Secret to Everybody" -r body.bin | cut -d' ' -f1)"
if [ "$header" = "$expected" ]; then echo "signature OK"; else echo "signature MISMATCH"; fi
the same MAC with openssl mac, and with a binary (hex) key
# OpenSSL 3's MAC command gives the same MAC, in upper-case hex
openssl mac -digest SHA256 -macopt key:"It's a Secret to Everybody" -in body.bin HMAC
# Binary secrets (e.g. from openssl rand -hex 32) go in as hexkey:
secret=$(openssl rand -hex 32)
openssl mac -digest SHA256 -macopt hexkey:$secret -in body.bin HMAC

How it works

  1. Save the body exactly as it was sent — printf '%s' writes it without a trailing newline — and run openssl dgst -sha256 -hmac "<secret>" -r body.bin; -r prints <hex> *body.bin, and cut keeps the hex.
  2. For GitHub's example (Hello, World! with the secret It's a Secret to Everybody) the result is 757107ea…3e17, so the header is sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17.
  3. To verify, recompute over the captured body and compare with the header value; the run also shows a one-character change in the body producing a mismatch.
  4. openssl mac -digest SHA256 -macopt key:<secret> -in body.bin HMAC is OpenSSL 3's dedicated MAC command; -macopt hexkey:<hex> passes a binary secret such as the output of openssl rand -hex 32.

Gotchas

  • echo 'Hello, World!' | openssl dgst … signs Hello, World!\n: the MAC becomes 8fde2e97…2325 instead of 757107ea…3e17. Use printf '%s' or the captured file, never echo.
  • openssl mac takes the algorithm name as its last argument: without the trailing HMAC it stops with mac: Missing argument: MAC name.
  • openssl dgst prints lower-case hex and openssl mac upper-case (757107EA…); providers send lower-case, so a plain string comparison against openssl mac output fails unless you normalise the case.
  • -hmac uses its argument's characters as the key. A secret generated as hex (openssl rand -hex 32) gives a different MAC as text than as the 32 bytes it encodes — the run shows both; when the provider means the bytes, pass -macopt hexkey:<hex>.

Related recipes