Crypto recipe

Message authentication with HMAC

Authenticate a message with HMAC-SHA256 and verify it in constant time.

An HMAC proves a message came from someone holding the shared secret key and was not modified in transit. It is the right tool for signed cookies, webhook signatures, API request signing, and download integrity.

HMAC-SHA256 is the standard choice. The one rule people break: verify the tag with a constant-time comparison, never with ==.

Get it right

  • Use a high-entropy random key (32 bytes for HMAC-SHA256), not a password.
  • Verify tags in constant time with the provided comparator — never ==.
  • HMAC authenticates but does not encrypt; if you also need secrecy, use AES-GCM.
  • A MAC is not a signature — both sides share the key, so it proves nothing to a third party.

Implementation

Setup Standard library (crypto/hmac).

Go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"fmt"
)

func main() {
	key := []byte("a 32-byte secret key goes here..")
	message := []byte("transfer $100 to alice")

	mac := hmac.New(sha256.New, key)
	mac.Write(message)
	tag := mac.Sum(nil) // 32-byte authentication tag

	// Verify: recompute, then compare with hmac.Equal (constant-time).
	check := hmac.New(sha256.New, key)
	check.Write(message)
	valid := hmac.Equal(tag, check.Sum(nil))
	fmt.Println("valid:", valid)
}

Setup Standard library (hmac).

Python
import hashlib
import hmac

key = b"a 32-byte secret key goes here.."
message = b"transfer $100 to alice"

tag = hmac.new(key, message, hashlib.sha256).digest()

# Verify — compare_digest is constant-time, defeating timing attacks.
expected = hmac.new(key, message, hashlib.sha256).digest()
valid = hmac.compare_digest(tag, expected)

Setup Built in (node:crypto).

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

const key = Buffer.from('a 32-byte secret key goes here..');
const message = 'transfer $100 to alice';

const tag = createHmac('sha256', key).update(message).digest();

// Verify — timingSafeEqual is constant-time, but throws on unequal lengths, so guard first.
const expected = createHmac('sha256', key).update(message).digest();
const valid = tag.length === expected.length && timingSafeEqual(tag, expected);

Setup Built in. HMACSHA256.HashData is .NET 6+.

.NET
using System.Security.Cryptography;
using System.Text;

byte[] key = Encoding.UTF8.GetBytes("a 32-byte secret key goes here..");
byte[] message = Encoding.UTF8.GetBytes("transfer $100 to alice");

byte[] tag = HMACSHA256.HashData(key, message);

// Verify in constant time.
byte[] expected = HMACSHA256.HashData(key, message);
bool valid = CryptographicOperations.FixedTimeEquals(tag, expected);

Setup libsodium.

C++
#include <sodium.h>
#include <string>

int main() {
    if (sodium_init() < 0) return 1;

    unsigned char key[crypto_auth_hmacsha256_KEYBYTES]; // 32
    crypto_auth_hmacsha256_keygen(key);

    std::string msg = "transfer $100 to alice";
    unsigned char tag[crypto_auth_hmacsha256_BYTES];    // 32
    crypto_auth_hmacsha256(tag,
        reinterpret_cast<const unsigned char *>(msg.data()), msg.size(), key);

    // Verify in constant time (returns 0 on a match).
    bool valid = crypto_auth_hmacsha256_verify(tag,
        reinterpret_cast<const unsigned char *>(msg.data()), msg.size(), key) == 0;
}

Setup Built in (javax.crypto.Mac).

Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

byte[] keyBytes = "a 32-byte secret key goes here..".getBytes(StandardCharsets.UTF_8);
byte[] message = "transfer $100 to alice".getBytes(StandardCharsets.UTF_8);

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(keyBytes, "HmacSHA256"));
byte[] tag = mac.doFinal(message);

// Verify — MessageDigest.isEqual is the JDK's constant-time comparator.
Mac check = Mac.getInstance("HmacSHA256");
check.init(new SecretKeySpec(keyBytes, "HmacSHA256"));
boolean valid = MessageDigest.isEqual(tag, check.doFinal(message));

Setup Cargo.toml: hmac = "0.12", sha2 = "0.10".

Rust
use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

fn main() {
    let key = b"a 32-byte secret key goes here..";
    let message = b"transfer $100 to alice";

    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
    mac.update(message);
    let tag = mac.finalize().into_bytes();

    // Verify — verify_slice is constant-time and returns Err on a mismatch.
    let mut check = HmacSha256::new_from_slice(key).unwrap();
    check.update(message);
    let valid = check.verify_slice(&tag).is_ok();
}