How to Use HMAC in Go

Compute an HMAC in Go by importing crypto/hmac and crypto/sha256, creating a new HMAC instance with your key, writing your message, and calling Sum to get the result.

The webhook signature problem

You're building a webhook endpoint. A payment service sends a POST request with order details. Before you process the payment, you need to be sure the request actually came from the payment service and not a random script on the internet. You share a secret key with the service. They sign the payload with that key. You verify the signature. If the math checks out, the message is authentic. That math is HMAC.

What HMAC actually does

HMAC stands for Hash-based Message Authentication Code. It combines a cryptographic hash function with a secret key. The result is a short, fixed-size string that acts like a digital fingerprint of the message. Anyone can compute the fingerprint if they have the key. If the message changes by even one bit, the fingerprint changes completely. If you don't have the key, you can't forge a valid fingerprint.

HMAC proves two things. The message hasn't been tampered with. The sender knows the secret key. It does not encrypt the message. The payload remains readable. HMAC is about trust, not secrecy.

HMAC proves integrity and authenticity. It does not hide the content.

Minimal computation

Here's the simplest HMAC computation: create the hasher with a key, write the message, and extract the digest.

package main

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

// ComputeHMAC calculates the HMAC-SHA256 of a message using a secret key.
func ComputeHMAC(key, message []byte) []byte {
	// hmac.New takes a constructor function for the hash and the secret key.
	// sha256.New returns a new SHA-256 hash instance that implements io.Writer.
	h := hmac.New(sha256.New, key)
	// Write feeds the message data into the hash state.
	// You can call Write multiple times to stream large payloads.
	h.Write(message)
	// Sum(nil) finalizes the hash and returns the digest bytes.
	// Passing nil appends to an empty slice, returning a fresh byte slice.
	return h.Sum(nil)
}

func main() {
	key := []byte("super-secret-key-do-not-share")
	message := []byte("payload-to-verify")

	mac := ComputeHMAC(key, message)
	fmt.Printf("HMAC: %x\n", mac)
}

How the pieces fit together

The hmac.New function expects a constructor that returns a hash instance. You pass sha256.New, not sha256.Sum256. The HMAC algorithm needs a stream interface so it can mix the key into the hash state incrementally. The constructor returns a value that implements io.Writer. You write the message data to that writer. The hash function processes the data in blocks. Finally, Sum computes the final digest.

The key is mixed into the hash state using a specific algorithm that prevents length-extension attacks. This is why you don't just hash the key and message together. The HMAC construction adds inner and outer padding to the key before hashing. The crypto/hmac package handles this correctly. You just provide the key and the hash constructor.

The output length depends on the hash function. SHA-256 produces a 32-byte digest. SHA-512 produces 64 bytes. The digest is always the same length regardless of the message size.

HMAC works with streams. You don't need to load the whole file into memory. You can pipe a file directly into the HMAC writer. This keeps memory usage constant regardless of file size.

Realistic verification

Real-world code involves HTTP requests, hex-encoded signatures, and constant-time comparison. Here's a verification function that handles decoding and safe comparison.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

// VerifySignature compares a provided hex-encoded signature against a computed HMAC.
// It uses constant-time comparison to prevent timing attacks.
func VerifySignature(key, payload, hexSig []byte) bool {
	// Compute the expected HMAC using the same key and payload.
	mac := hmac.New(sha256.New, key)
	mac.Write(payload)
	expected := mac.Sum(nil)

	// Decode the hex string from the request into bytes.
	// hex.Decode returns an error if the string contains invalid hex characters.
	actual, err := hex.DecodeString(string(hexSig))
	if err != nil {
		return false
	}

	// hmac.Equal performs a constant-time comparison.
	// Standard == operator leaks timing information that attackers can exploit.
	return hmac.Equal(expected, actual)
}

Now wrap it in an HTTP handler. The handler reads the body, extracts the signature from the header, and calls the verification function.

package main

import (
	"io"
	"net/http"
)

// handleWebhook verifies the HMAC signature and processes the request.
func handleWebhook(w http.ResponseWriter, r *http.Request) {
	// Read the entire body to compute the signature.
	// Some services sign the raw body, others sign a JSON representation.
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "read error", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	key := []byte("webhook-secret")
	sig := []byte(r.Header.Get("X-Signature"))

	if !VerifySignature(key, body, sig) {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}

	w.WriteHeader(http.StatusOK)
}

Pitfalls and compiler errors

Timing attacks are the most common mistake. If you use bytes.Equal or the == operator to compare signatures, the comparison stops at the first mismatch. An attacker can measure response times to guess the signature byte by byte. hmac.Equal takes the same amount of time regardless of where the mismatch occurs. Always use hmac.Equal for cryptographic comparisons.

Key management is another risk. Hardcoding keys in source code is dangerous. Load keys from environment variables or a secrets manager. The if err != nil pattern is verbose by design. It forces you to handle the error immediately. In cryptographic code, ignoring an error can open a backdoor.

The compiler catches type mismatches early. If you pass a string to hmac.New instead of []byte, the compiler rejects it with cannot use key (untyped string) as []byte value in argument. Convert strings to byte slices explicitly. If you try to use the digest as a string without encoding, you get cannot use mac (type []byte) as string value in argument. Use hex.EncodeToString or base64 to convert the digest for transmission.

Don't use weak hash functions. SHA-1 and MD5 are broken for security purposes. Use SHA-256 or SHA-512. The crypto/sha256 and crypto/sha512 packages are the standard choices.

Timing attacks are silent. Always use hmac.Equal for cryptographic comparisons.

When to use HMAC

Use HMAC when you need to verify integrity and authenticity with a shared secret key. Use RSA or Ed25519 digital signatures when you need public-key verification, so anyone can verify without sharing a secret. Use AES-GCM encryption when you need to hide the message content, not just verify it. Use a simple hash like SHA-256 without a key when you only need a checksum and don't care about authentication. Use crypto/rand to generate keys. Never use predictable strings.

Where to go next