Hashing & MACs (SHA-2/SHA-3, HMAC)
What collision, preimage and second-preimage resistance actually promise, the sponge construction behind SHA-3, and why authentication needs a keyed MAC compared in constant time — not a hash.
A cryptographic hash compresses arbitrary input to a fixed-size digest such that finding structure in that map is infeasible. It is the most reused primitive in the field — signatures sign a hash, KDFs are built from one, commitments and Merkle trees are hashes all the way down — and also the most misapplied, because "hash" gets reached for when the actual requirement is authentication, which needs a key.
The three properties, and which one you need #
| Property | Attacker must find | Generic cost (-bit output) | Broken for |
|---|---|---|---|
| Preimage resistance | any with , given | none of MD5/SHA-1 in practice | |
| Second-preimage | with , given | none in practice | |
| Collision resistance | any with equal digests | (birthday) | MD5, SHA-1 |
SHA-2 and SHA-3 are different constructions #
SHA-2 (FIPS 180-4) is Merkle–Damgård: chained compression over padded blocks. SHA-256 and SHA-512 differ in word size (32 vs 64 bit) and rounds (64 vs 80), which makes SHA-512 faster than SHA-256 on 64-bit CPUs despite the larger output. The truncated variants SHA-512/224 and SHA-512/256 are not just shortened output — because the internal state exceeds the digest, they are immune to length extension.
SHA-3 (FIPS 202) is Keccak, a sponge:
flowchart LR
subgraph Absorb
M1["M1"] --> X1((XOR)) --> f1["f"] --> X2((XOR))
M2["M2"] --> X2 --> f2["f"]
end
subgraph Squeeze
f2 --> Z1["Z1"] --> f3["f"] --> Z2["Z2"]
end
State is split into a rate (touched by input/output) and a capacity (never touched directly); security is . Because output is squeezed from a state larger than the digest, length extension is impossible by construction. The sponge also yields XOFs — SHAKE128/SHAKE256 produce arbitrary-length output, which is what post-quantum schemes use for expansion.
For speed, BLAKE3 is the current answer: a Merkle tree of ChaCha-derived compression, unlimited parallelism, and several GB/s per core with native keyed-hash and derive_key modes.
MACs: hashing with a key #
A hash provides integrity only against accidental corruption. Against an adversary it provides nothing — the attacker recomputes the digest over their modified message. Authentication needs a MAC: a keyed function where forging a tag without the key is infeasible.
HMAC (RFC 2104) is the standard fix:
The nested outer hash conceals the inner chaining state, which is precisely what defeats extension. HMAC's security proof needs only that the compression function is a PRF — not collision resistance — which is why HMAC-SHA1 remains secure even though SHA-1 signatures are dead. It works with any hash, is available everywhere, and is FIPS-approved.
Alternatives worth knowing: KMAC (SHA-3 native, no nesting needed), BLAKE3 keyed mode, Poly1305 and GMAC (one-time MACs inside AEAD constructions — never reuse their key across messages), and CMAC for AES-only hardware.
Choosing #
| Need | Use |
|---|---|
| General-purpose digest | SHA-256, or SHA-512/256 on 64-bit |
| Maximum speed, non-FIPS | BLAKE3 |
| Length-extension immunity | SHA-3, BLAKE2/3, or SHA-512/256 |
| Authenticate a message with a shared key | HMAC-SHA256 |
| Authenticate and encrypt | AEAD — AES-GCM or ChaCha20-Poly1305 |
| Derive keys from a strong secret | HKDF |
| Hash a password | none of the above — Argon2id |
| Non-adversarial checksum | xxHash, CRC32 — not cryptographic, and much faster |
That last pair is worth stating plainly: a fast hash is exactly the wrong tool for passwords, because the attacker's advantage is speed. And a cryptographic hash is overkill for detecting disk corruption. Matching the primitive to the threat is most of the skill here.