Introduction

Imagine you receive a message claiming to be from your bank. It looks legitimate. But how do you know nobody tampered with it on the way to you?

A plain hash like SHA-256 doesn't help by itself. Anyone can hash a message — there is no secret involved. What you need is a way to prove the message was created (or at least approved) by someone who holds a shared secret key. That is exactly what HMAC does.

HMAC (Hash-based Message Authentication Code) was standardized in RFC 2104 (1997) by Hugo Krawczyk, Mihir Bellare, and Ran Canetti. It wraps any cryptographic hash function — SHA-256, SHA-3, whatever — with a key to produce a short tag. Send the message and its tag together. The receiver re-computes the tag with the same key; if the values match, the message is authentic and unmodified.

The magic: without the key you cannot compute a valid tag. Not because we made the hash function secret — it is still public SHA-256 — but because the key is woven into the computation in a way that makes forgery as hard as breaking the underlying hash.

Try It

Enter a message and a secret key, then sign it. After that, flip a single bit inside the message and try to verify — the tag will refuse to match.

<p class="hint">{{hint}}</p>
<div class="field-row">
  <label>{{label_message}}</label>
  <input id="msg" type="text" value="{{default_msg}}" autocomplete="off" spellcheck="false">
</div>
<div class="field-row">
  <label>{{label_key}}</label>
  <input id="key" type="text" value="{{default_key}}" autocomplete="off" spellcheck="false">
</div>
<div class="btns">
  <button id="signBtn" type="button">{{btn_sign}}</button>
  <button id="verifyBtn" type="button" disabled>{{btn_verify}}</button>
  <button id="resetBtn" type="button" class="ghost">{{btn_reset}}</button>
</div>
<div id="tagBox" class="tag-box hidden">
  <div class="tag-label">{{tag_label}}</div>
  <div id="tagVal" class="tag-val"></div>
</div>
<div class="tamper-row hidden" id="tamperRow">
  <label>{{tamper_label}}</label>
  <div id="bitGrid" class="bit-grid"></div>
  <div class="tamper-hint">{{tamper_hint}}</div>
</div>
<div id="status" class="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.field-row { display: flex; flex-direction: column; margin-bottom: .6rem; gap: .2rem; }
label { font-size: .8rem; font-weight: 600; color: #555; }
input { font: 15px ui-monospace, monospace; padding: .4rem .6rem; border: 1px solid #ccc;
        border-radius: 6px; width: 100%; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .7rem 0; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .45; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
.tag-box { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px;
           padding: .6rem .8rem; margin: .5rem 0; }
.tag-label { font-size: .75rem; font-weight: 700; color: #555; margin-bottom: .25rem; }
.tag-val { font: 700 13px ui-monospace, monospace; color: #1d3557; word-break: break-all; }
.hidden { display: none; }
.tamper-row { margin: .6rem 0; }
.bit-grid { display: flex; flex-wrap: wrap; gap: 3px; margin: .35rem 0 .3rem; }
.bit { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
       font: 700 11px ui-monospace, monospace; border-radius: 4px; cursor: pointer;
       background: #c9ccd1; border: 1px solid #adb1b8; user-select: none; transition: background .1s; }
.bit:hover { background: #b0b5bc; }
.bit.flipped { background: #e63946; border-color: #c92f3c; color: #fff; }
.tamper-hint { font-size: .78rem; color: #666; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.5em; margin-top: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
// Code not found

Notice the avalanche effect: changing even one bit in the message produces a completely different tag. An attacker who intercepts the message and tweaks it cannot produce a valid tag without knowing the key. And crucially, HMAC also defeats length-extension attacks that plague plain SHA-256 — you cannot append extra data to a message and compute a valid new tag without the key.

The Real Complexity

Why not just prepend the key and hash? hash(key || message) sounds plausible, but it is broken for Merkle-Damgård hashes like SHA-256. An attacker who knows only the tag can extend the message and compute a valid new tag — the infamous length-extension attack — without knowing the key at all.

HMAC avoids this with a nested double-pass construction:

HMAC(K, M) = H( (K ⊕ opad) || H( (K ⊕ ipad) || M ) )
  • ipad and opad are fixed byte patterns (0x36 and 0x5c repeated to the block size).
  • The inner hash compresses the message with a derived key.
  • The outer hash wraps the inner result with a second derived key, cutting off any extension attempt.

Provable security: Bellare, Canetti, and Krawczyk proved in 1996 that HMAC is a secure pseudorandom function (PRF) if the compression function of the underlying hash is a PRF. In practice, SHA-256-based HMAC with a 256-bit key is considered computationally infeasible to forge.

Status: solved/well-understood — HMAC is not NP-hard or undecidable; it is a provably secure primitive whose security reduces to the hash function. See also factoring for the number-theoretic hardness that underlies other crypto primitives.

Where It Matters

HMAC is one of the most widely deployed cryptographic primitives on the internet:

  • API authentication: services like AWS, Stripe, and GitHub sign API requests with HMAC-SHA256. Your client signs the request body + timestamp; the server rejects anything with a bad tag, blocking replay and tampering.
  • Cookies and sessions: web frameworks sign session cookies with HMAC so a user cannot forge admin privileges by editing the cookie value.
  • JWT (JSON Web Tokens): the HS256 algorithm in JWTs is HMAC-SHA256. The signature field is the HMAC tag over the header and payload.
  • TLS 1.2: the Finished message in a TLS handshake uses a PRF built on HMAC to prove both sides computed the same master secret.
  • Software updates: package managers and firmware distributors publish HMAC tags alongside binaries so that downloaders can verify the file was not modified in transit.

The common thread: any time you need to prove "this data came from someone who knows the key and hasn't been modified", HMAC is the standard answer. See how discrete logarithms and factoring underpin the related area of digital signatures.

Conclusion

A plain hash tells you a message's fingerprint; HMAC tells you whose fingerprint it is. By weaving a secret key into a nested double-pass construction, it converts a public hash function into an unforgeable seal — and its security is not just an assumption but a mathematical reduction to the underlying hash.

The next time you sign into an API, check a JWT, or install a software update, HMAC is almost certainly the quiet guardian making sure what you received is exactly what was sent. Two lines of math, three decades of practice, and the internet's data integrity depends on it.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/hmac/Content licensed under CC BY-NC 4.0.