Learning outcome
You will be able to explain what HMAC guarantees, distinguish it from hashing, encryption, and digital signatures, and implement HMAC-SHA-256 verification with server-side key handling and timing-safe comparison. You will also recognize replay, rotation, and canonicalization concerns that HMAC alone does not solve.
Intuition
A plain hash detects accidental changes only when the trusted hash is protected separately. An attacker who can replace both a message and its ordinary hash can simply calculate a new hash.
HMAC combines a cryptographic hash with a secret key. A party without that key cannot feasibly produce a valid authentication tag for a modified message. It provides integrity and symmetric authentication, but it does not hide the message.
Encryption provides confidentiality and may provide integrity when an authenticated-encryption mode is used. A digital signature uses an asymmetric private/public key pair, supports public verification, and can distinguish the signer from verifiers. HMAC is symmetric: every verifier holding the key can also generate tags, so it does not provide non-repudiation.
Deep dive
HMAC-SHA-256 accepts an arbitrary byte sequence and a secret key, producing a 32-byte tag. Authentication applies to bytes, not abstract objects. Both sides must therefore agree on the exact UTF-8 encoding, field order, separators, number formatting, casing, Unicode normalization, and treatment of whitespace.
For structured data, define one canonical representation or authenticate the original request bytes before parsing. Avoid ambiguous concatenation such as adjacent variable-length fields; use explicit framing, length prefixes, or an unambiguous standardized serialization. Include security-relevant metadata such as a timestamp, nonce, HTTP method, resource identifier, and content type when the protocol requires them.
Generate production keys with a cryptographically secure random source and store them in a server-side secret manager, key vault, or similarly protected configuration channel. Do not embed production keys in source, send them to clients, or write them to logs. Use separate keys for separate protocols or purposes to limit cross-protocol misuse.
For rotation, attach a non-secret key identifier to each authenticated envelope. Sign new messages with the current key and temporarily retain narrowly scoped previous keys for verification. Bound the overlap, monitor old-key use, and retire compromised keys promptly. Select a key by identifier rather than trying an unbounded key collection.
HMAC does not prevent replay. A captured message and tag remain valid unless the authenticated data contains freshness information and the verifier enforces an expiration window or records one-time nonces.
Failure modes
- Using an unkeyed SHA-256 digest as though it authenticated an attacker-controlled message.
- Comparing tags with ordinary equality, an early-exit loop, or encoded strings rather than a timing-safe byte comparison.
- Computing the tag over parsed data while the sender authenticates original bytes.
- Authenticating a body but omitting routing, identity, amount, timestamp, or other security-relevant context.
- Reusing one HMAC key across unrelated protocols or tenants without a deliberate key hierarchy.
- Logging keys, authorization headers, or diagnostic objects that contain secret material.
- Rotating immediately without a controlled verification overlap, or accepting old keys forever.
- Treating a valid tag as proof of freshness, confidentiality, or a particular public signer.
Before fixed-time comparison, a production protocol should decode the supplied tag strictly and require the expected 32-byte HMAC-SHA-256 length. Rejecting a malformed public length is acceptable; compare valid equal-length candidate tags with FixedTimeEquals.
Production code
The executable example uses a deterministic fixture key so its output is stable. That key is intentionally not a production secret. A deployed service should obtain key bytes from a server-side secret provider, associate them with a key identifier, avoid converting them to loggable strings, and clear mutable buffers when practical.
The program authenticates one exact UTF-8 message and then performs three checks: the original message succeeds, a changed message fails, and the original message with a different key fails. It never prints either key or the authentication tag.
Code walkthrough
HMACSHA256.HashData computes the tag over the UTF-8 bytes. Verification independently recomputes the candidate tag and passes the two byte sequences to CryptographicOperations.FixedTimeEquals; ordinary sequence equality is not used. Temporary computed tags and fixture key arrays are cleared after use.
The changed-message test demonstrates integrity, while the wrong-key test demonstrates symmetric key possession. Neither test demonstrates replay prevention or confidentiality. In a real protocol, verification should occur before acting on the authenticated message, and failure responses should avoid revealing unnecessary diagnostic distinctions.
Interview drill
- Why can an attacker replace both a message and its plain SHA-256 hash, but not normally produce a valid HMAC without the key?
- Why does HMAC provide no confidentiality or non-repudiation?
- Why is
FixedTimeEqualspreferable to ordinary equality for authentication tags? - Which fields would you authenticate for a signed webhook request, and how would you prevent replay?
- How would a key identifier and bounded overlap support rotation without testing every historical key?
Exercise: Extend the protocol design on paper to authenticate a timestamp, nonce, method, path, and body. Specify an unambiguous byte framing, an allowed clock skew, nonce retention, exact tag encoding, and rotation behavior. Do not merely concatenate variable-length strings.
Revision checklist
- Use HMAC, not a plain hash, for symmetric message authentication.
- Authenticate a precisely specified byte representation and all relevant context.
- Keep strong random keys server-side; never log, expose, or commit them.
- Separate keys by purpose and identify versions with non-secret key IDs.
- Decode tags strictly, enforce the algorithm's tag length, and compare with
FixedTimeEquals. - Add authenticated timestamps or nonces plus server-side enforcement when replay matters.
- Rotate with a short monitored overlap and an explicit retirement plan.
- Use authenticated encryption when confidentiality is also required; use signatures when public verification is required.
<!-- tin:verified-code:start -->
Executable code examples
Authenticate and verify a UTF-8 message with HMAC-SHA-256
Program.cs
using System;
using System.Security.Cryptography;
using System.Text;
internal static class Program
{
private static void Main()
{
// Deterministic test fixtures only. Load production keys from a
// server-side secret provider, and never log them.
byte[] key = Convert.FromHexString(
"000102030405060708090A0B0C0D0E0F" +
"101112131415161718191A1B1C1D1E1F");
byte[] wrongKey = Convert.FromHexString(
"202122232425262728292A2B2C2D2E2F" +
"303132333435363738393A3B3C3D3E3F");
const string message = "orderId=123&amount=49.95¤cy=USD";
try
{
byte[] tag = HMACSHA256.HashData(
key,
Encoding.UTF8.GetBytes(message));
try
{
Console.WriteLine($"Valid message: {Verify(key, message, tag)}");
Console.WriteLine($"Changed message: {Verify(key, message + "&rush=true", tag)}");
Console.WriteLine($"Wrong key: {Verify(wrongKey, message, tag)}");
}
finally
{
CryptographicOperations.ZeroMemory(tag);
}
}
finally
{
CryptographicOperations.ZeroMemory(key);
CryptographicOperations.ZeroMemory(wrongKey);
}
}
private static bool Verify(
ReadOnlySpan<byte> key,
string message,
ReadOnlySpan<byte> expectedTag)
{
byte[] actualTag = HMACSHA256.HashData(
key,
Encoding.UTF8.GetBytes(message));
try
{
return CryptographicOperations.FixedTimeEquals(
actualTag,
expectedTag);
}
finally
{
CryptographicOperations.ZeroMemory(actualTag);
}
}
}
<!-- tin:verified-code:end -->