How to Use HMAC for Message Authentication in Go
You are building a service that receives webhooks from a payment provider. The provider sends a JSON payload saying a payment succeeded. Your code updates the user's balance. Someone intercepts the request, changes the amount, and resends it. Your code trusts the payload and credits the wrong amount. The problem isn't encryption; anyone can read the message. The problem is authenticity. You need a way to prove the message came from the provider and hasn't been touched. That's where HMAC comes in.
HMAC stands for Hash-based Message Authentication Code. It combines a cryptographic hash function with a secret key to produce a fixed-size tag. The tag acts as a digital signature for the message. If you have the key, you can generate the tag. If you verify the tag with the same key, you know the message is intact and came from someone who holds the key.
Think of HMAC as a wax seal on a letter. You press a unique ring into the wax. If someone opens the letter and changes the contents, the wax breaks. If someone tries to forge the seal, the ring won't match. HMAC does this digitally. The hash function provides the wax, and the secret key provides the unique ring.
The crypto/hmac package implements the algorithm defined in RFC 2104. It works with any hash function that implements the hash.Hash interface. SHA-256 is the standard choice for most applications.
Minimal HMAC generation
Here is the simplest code to generate an HMAC digest. It creates an HMAC instance, writes the message, and returns the result.
package main
import (
"crypto/hmac"
"crypto/sha256"
"fmt"
)
func generateHMAC(key, message []byte) []byte {
// hmac.New takes a hash constructor and the secret key.
// sha256.New returns a new SHA-256 hash instance.
h := hmac.New(sha256.New, key)
// Write the message bytes into the HMAC instance.
// This updates the internal state of the hash computation.
h.Write(message)
// Sum(nil) finalizes the hash and returns the digest bytes.
// Passing nil creates a new slice for the result.
return h.Sum(nil)
}
func main() {
key := []byte("super-secret-key")
message := []byte("Hello, World!")
digest := generateHMAC(key, message)
fmt.Printf("HMAC: %x\n", digest)
}
The hmac.New function expects a constructor function that returns a hash.Hash. You pass sha256.New, not sha256.Sum256. The constructor creates a fresh hash state for each HMAC computation. If you pass sha256.Sum256, which returns a byte array, the compiler rejects the code with cannot use sha256.Sum256 (func([]byte) [32]byte) as func() hash.Hash value in argument.
HMAC does not just concatenate the key and message. That construction is vulnerable to length extension attacks. Instead, HMAC uses a two-pass structure. It hashes the key mixed with an inner padding, then hashes that result mixed with the key and an outer padding. This ensures an attacker cannot extend the message and compute a valid tag without the key.
The hash function does the math. The key does the trust.
Verification and timing attacks
Generating the HMAC is only half the work. You must verify the tag when you receive a message. Verification requires recomputing the HMAC and comparing it to the received tag. The comparison must be constant-time.
Standard equality checks like == or bytes.Equal stop as soon as they find a mismatch. This leaks timing information. An attacker can measure how long the comparison takes to guess the hash byte-by-byte. If the first byte matches, the comparison takes slightly longer. If the first two bytes match, it takes even longer. Over many requests, the attacker can reconstruct the valid HMAC.
Use hmac.Equal for all comparisons. It compares every byte regardless of mismatches, preventing timing leaks.
Here is a verification function that follows the convention.
func verifyHMAC(key, message, receivedHMAC []byte) bool {
// Recreate the HMAC instance with the same key and hash function.
h := hmac.New(sha256.New, key)
h.Write(message)
// Compute the expected digest.
expected := h.Sum(nil)
// hmac.Equal performs a constant-time comparison.
// This prevents timing attacks where an attacker measures
// comparison duration to guess the hash byte-by-byte.
return hmac.Equal(expected, receivedHMAC)
}
The community expects constant-time comparison for secrets. Using bytes.Equal for HMAC verification is a red flag in code reviews. Timing attacks are silent. Always use hmac.Equal.
Key management and encoding
HMAC is only as strong as the key. A weak key turns the algorithm into a suggestion. Never hardcode keys. Never use predictable strings like "password123". Generate keys using crypto/rand.
The key length should match the security level of your application. For SHA-256, a 32-byte key is standard. HMAC handles keys of any length by hashing or padding them internally, but using a key length equal to the hash output size is efficient and secure.
Here is how to generate a secure key.
import (
"crypto/rand"
)
func generateKey() ([]byte, error) {
// Allocate a 32-byte slice for the key.
key := make([]byte, 32)
// Fill the slice with cryptographically secure random bytes.
// rand.Read blocks until the required entropy is available.
_, err := rand.Read(key)
if err != nil {
// rand.Read fails only if the system entropy source is exhausted.
// This is rare but indicates a serious system issue.
return nil, err
}
return key, nil
}
HMAC returns raw bytes. You cannot send raw bytes in JSON or HTTP headers directly. You must encode them. Hex encoding is common for debugging and short tokens. Base64 is more compact for storage. Use encoding/hex or encoding/base64.
When encoding, be consistent. If you encode the HMAC as hex on the sender side, decode it back to bytes on the receiver side before verification. Comparing encoded strings directly can introduce case-sensitivity issues or padding mismatches. Compare bytes whenever possible.
If you forget to import crypto/rand, the compiler complains with undefined: rand. If you pass a non-slice to rand.Read, you get a type mismatch error like cannot use key (variable of type string) as []byte value in argument.
A weak key is a broken lock. Generate randomness, store secrets securely, and rotate keys periodically.
Pitfalls and compiler errors
HMAC is straightforward, but a few traps exist.
Timing attacks are the most common runtime issue. Using == or bytes.Equal for verification exposes your system to byte-by-byte guessing. The fix is always hmac.Equal.
Key reuse across different purposes can be dangerous. If you use the same key for HMAC and another protocol, a compromise in one might leak the key for the other. Derive separate keys for different contexts using a key derivation function like HKDF, or use distinct random keys.
The hmac package implements the hash.Hash interface. This means you can use an HMAC instance anywhere a hash is expected. You can pipe data into it, or use it with io.Copy. This is useful for streaming large messages without loading them entirely into memory.
import (
"io"
)
func hmacFromReader(key []byte, r io.Reader) ([]byte, error) {
h := hmac.New(sha256.New, key)
// io.Copy streams data from the reader into the hash.
// This avoids buffering the entire message in memory.
if _, err := io.Copy(h, r); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
If you try to use a value type where a function is expected, the compiler catches it. Passing sha256.New is correct. Passing sha256 (the package) or a hash instance directly causes an error. The compiler message is explicit: cannot use sha256 (variable of type *sha256.digest) as func() hash.Hash value in argument.
Don't fight the type system. Pass the constructor, not the instance.
When to use HMAC
Authentication primitives serve different threat models. Pick the right tool for your scenario.
Use HMAC when you need to verify integrity and authenticity between two parties that share a secret key. This is the standard pattern for webhooks, API tokens, and session validation.
Use digital signatures like RSA or ECDSA when you need non-repudiation or when parties do not share a secret key in advance. Signatures allow anyone to verify the message using a public key, while only the holder of the private key can sign.
Use simple hashing like SHA-256 when you only need to check for accidental corruption and do not care about malicious tampering. Hashing provides no authentication; anyone can compute the hash.
Use encryption like AES-GCM when you need to hide the message content. Encryption provides confidentiality. GCM also provides authentication, but HMAC is often simpler when you only need to authenticate data that is already encrypted or public.
Authentication matches the threat model. Choose the primitive that fits your trust boundaries.