Secrets, PKI & CertificatesIII · Cryptography for Infrastructure EngineersCryptography
Hashes, MACs and HMAC: integrity is not authenticity
What you'll learn
- State the three hash properties that operational tooling depends on
- Explain why an unkeyed digest published beside its artefact proves nothing against an attacker
- Describe what a MAC adds, what it cannot provide, and why HMAC is constructed as it is
- Diagnose the common production failures of MAC verification, including body rewriting and timing-unsafe comparison
Prerequisites
Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26
Integrity means the bytes were not altered. Authenticity means a particular party produced them. Tooling blurs the two constantly, because both are delivered as a short hexadecimal string that either matches or does not. The difference is whether a key was involved, and the difference decides whether an attacker who controls your distribution path can walk straight through the check.
What a hash gives you, and what it costs
A cryptographic hash maps any input to a fixed-length digest. Operational tooling leans on exactly three properties.
- Determinism. The same bytes always produce the same digest, on any machine, in any decade. This is what makes a digest a usable name for content.
- Preimage resistance. Given a digest, you cannot construct an input that produces it. This is what stops a digest from being a reversible encoding of the value it covers.
- Collision resistance. You cannot find two different inputs with the same digest. This is the property MD5 and SHA-1 lost, which is why signatures over them stopped being trustworthy long before anyone stopped seeing them in scripts.
Hashes are everywhere in infrastructure precisely because they need no key: content addressing in Git and in container registries, certificate fingerprints, SSH key fingerprints, cache keys, deduplication. A fingerprint you compare by eye is a hash:
# Two ways to fingerprint the same public key material.
openssl x509 -in app.crt -noout -fingerprint -sha256
openssl x509 -in app.crt -noout -pubkey | openssl sha256
The first digests the whole certificate, so it changes on reissue. The second digests only the public key, so it survives reissue for the same key. Knowing which one your monitoring compares is the difference between an alert on every renewal and an alert on an actual key change.
The gap: integrity without authenticity
Consider the classic install pipeline: download an archive, compare its SHA-256 against a published value, unpack it. Ask who is defeated by that check.
flowchart LR
A["Artefact server"] --> B["Archive bytes"]
A --> C["Published digest"]
B --> D{"Digests match?"}
C --> D
D -- "yes" --> E["Install proceeds"]
D -- "no" --> F["Abort"]
G["Attacker with write\naccess to the server"] --> A
A corrupted disk on the mirror is defeated. A truncated download is defeated. An attacker who owns the artefact server is not defeated at all, because they control both inputs to the comparison. The check has integrity and no authenticity, and it is the authenticity you actually wanted. There are exactly two ways to close that gap: sign the digest with a key the attacker does not have, which is lesson 5, or compute the digest under a shared key that the attacker does not have, which is the rest of this lesson.
Message authentication codes
A MAC takes a message and a secret key and produces a tag. A verifier holding the same key recomputes the tag and compares. If they match, the message was produced by a holder of that key and has not been altered. Two consequences follow immediately.
The good one is that a MAC is fast, small and needs no certificates, which is why it protects webhook deliveries, session cookies, signed URLs, inter-service request signing and the authentication tag inside every AEAD you met in lesson 2.
The limiting one is that both parties hold the same key, so neither can prove to a third party which of them produced a tag. A MAC gives integrity and authenticity between the parties. It never gives non-repudiation. If your compliance requirement is “prove to an auditor that this system and not that one issued the instruction”, a MAC cannot do it and a signature can.
Why HMAC is shaped the way it is
The obvious construction, hashing the key concatenated with the message, is broken for the SHA-2 family. Those hashes build a digest by absorbing blocks into a running internal state and emitting that state at the end. An attacker who has one digest therefore has the internal state after your message, and can continue absorbing blocks of their own choosing to produce a valid digest for a longer message, without ever knowing the key. That is a length extension attack, and it turns an authentication check into an attacker-controlled append.
HMAC exists to remove the problem structurally. It derives two values from the key and hashes twice, feeding the result of the inner hash into the outer one, so the attacker never holds an internal state that corresponds to the key. The construction is specified in RFC 2104 and FIPS 198-1, is implemented in every platform library, and is not something you should ever write.
# The verification primitive, at the shell, for illustration.
KEYFILE=/etc/webhook/hmac.key
openssl dgst -sha256 -mac HMAC -macopt "hexkey:$(cat "$KEYFILE")" payload.json
Even in that one-line form the important decisions are visible.
The key comes from a file with restricted permissions rather than
from an environment variable that will end up in a process
listing. The digest is computed over payload.json as it was
received, byte for byte. And the output is a tag you must compare
in constant time, which the shell cannot do for you and which is
why this belongs in application code with a real library.
Where the distinction bites in production
Four failures recur often enough to be worth memorising.
- The proxy rewrote the body. A webhook signature is computed over exact bytes. An API gateway that reformats JSON, changes the charset, strips a trailing newline or decompresses the payload invalidates a perfectly good tag. Verify against the raw request body captured before any parsing, and if the gateway must transform, terminate and re-sign deliberately.
- The comparison leaked. Using a normal equality check on tags gives an attacker a timing signal that grows with each correct leading byte. With enough retries a tag can be constructed without the key. Every library ships a constant-time comparison for this reason.
- No freshness, so the delivery replayed. A valid tag stays valid forever. Signed request schemes include a timestamp inside the signed bytes and reject deliveries outside a narrow window, which is also why clock skew shows up as an authentication failure rather than as a time problem.
- The hash was mistaken for protection. Logging a digest of a token instead of the token is only safe when the token has enough entropy that guessing is infeasible. Digest a six-character coupon code and anyone can rebuild the table. A keyed HMAC removes the guessing attack because the attacker cannot compute candidate tags.
That last pattern has a production-grade example worth studying. An OpenBao audit device records every request and response, including denials, and it replaces sensitive fields with keyed digests rather than omitting or plainly hashing them:
{"time":"2026-08-26T21:26:20.555636512Z","type":"response",
"auth":{"client_token":"hmac-sha256:da33377eba1c...",
"policies":["app-read","default"],
"policy_results":{"allowed":false}},
"request":{"operation":"read","path":"kv/data/app/other",
"remote_address":"127.0.0.1"},
"error":"1 error occurred:\n\t* permission denied\n\n"}
Searching that log for the plaintext of the stored secret returns
nothing, because the secret is never written in clear. What is
written is enough to answer an investigator’s questions: which
path, which policies, allowed or denied, and a stable
hmac-sha256: value that lets you correlate every request made by
the same token without ever learning the token. Integrity,
authenticity and confidentiality are being traded deliberately,
and the keyed digest is what makes the trade possible.
Production discipline
- Never accept a digest from the same channel as the artefact. If the publisher can change both, the check is documentation, not a control.
- Verify the MAC before parsing. Parsing untrusted input is attack surface. The tag check is the cheapest gate you have and it belongs first.
- Use the library comparison function. Constant-time comparison is one call, and the alternative is a real, exploited class of bug.
- Put a timestamp inside the signed bytes. Then reject stale deliveries and monitor clock skew, because skew and forgery look identical from the receiving end.
- Give every shared MAC key an owner and a rotation plan. Shared keys spread. A webhook secret that six teams hold is a secret that has no owner and cannot be rotated without an outage nobody has scheduled.
Cross-course references
- Linux for Production Sysadmins - Part XII (RepoSecurity) covers signed package metadata, which is the working example of a digest made trustworthy by being signed rather than merely published.
- Kubernetes for Production Sysadmins - Part XCIV (AuditLogs) covers the cluster audit stream whose design faces the same problem the keyed audit record in this lesson solves.
- Git, CI/CD & GitOps for Infrastructure Engineers - Part LXVII (DependencyPinning) covers pinning by digest, and the limits of a digest whose source you do not authenticate.
Quiz
Knowledge check · 4 questions
Q1. A release page publishes an archive and a sha256sums.txt file on the same web server. What does verifying the digest actually defend against?
Q2. A MAC can prove integrity and origin between two parties but cannot prove to a third party which of them produced the message.
Q3. Why is HMAC constructed with two hashing passes instead of simply hashing the key followed by the message?
Q4. Explain the failure and the correct fix.
A payment provider posts webhooks with a signature header. Deliveries verified correctly for eight months. After an API gateway was introduced in front of the receiver on 14 August, roughly one delivery in three fails verification and is rejected. The failures cluster on payloads containing accented characters, and the provider insists it has not changed the signing key.
Passing score: 75%. Answers are checked in this browser.