Apps Artificial Intelligence CSS DevOps Go JavaScript Laravel Linux MongoDB MySQL PHP Python Rust Svelte Vue

Implement HMAC Message Authentication in Go

2 min read .
Implement HMAC Message Authentication in Go

When an application exchanges sensitive data, it is often important to verify both the integrity and authenticity of each message. If an attacker can modify a request in transit or replay an old valid request, the receiving service needs a reliable way to reject it.

One common building block is HMAC (Hash-based Message Authentication Code). This article shows how to generate and verify an HMAC in Go with SHA-512, plus a nonce and timestamp to reduce replay risk.

What Is HMAC?

HMAC combines a cryptographic hash function with a shared secret key. The sender calculates a message authentication code from the message and secret. The receiver repeats the calculation with the same secret and compares the result.

If the values match, the receiver knows that someone possessing the secret created the signature and that the signed data was not modified.

Example Implementation

package main

import (
	"crypto/hmac"
	"crypto/sha512"
	"encoding/hex"
	"fmt"
	"time"
)

func main() {
	message := "this is a sample message"
	key := "replace-with-a-strong-secret"
	nonce := "random-request-id"
	timestamp := time.Now().Unix()
	allowedTimestampDiff := int64(60)

	usedNonces := make(map[string]bool)

	generatedHMAC := generateMAC(message, key, nonce, timestamp)
	fmt.Println("Generated HMAC:", generatedHMAC)

	isValid := verifyHMAC(
		message,
		key,
		generatedHMAC,
		nonce,
		timestamp,
		allowedTimestampDiff,
		usedNonces,
	)
	fmt.Println("Valid HMAC:", isValid)

	fake := verifyHMAC(
		message,
		key,
		"incorrect",
		"different-nonce",
		timestamp,
		allowedTimestampDiff,
		usedNonces,
	)
	fmt.Println("Fake HMAC:", fake)
}

func generateMAC(message, key, nonce string, timestamp int64) string {
	data := fmt.Sprintf("%s:%d:%s", message, timestamp, nonce)
	h := hmac.New(sha512.New, []byte(key))
	_, _ = h.Write([]byte(data))
	return hex.EncodeToString(h.Sum(nil))
}

func verifyHMAC(
	message, key, receivedHMAC, nonce string,
	timestamp, allowedTimestampDiff int64,
	usedNonces map[string]bool,
) bool {
	expectedBytes, err := hex.DecodeString(generateMAC(message, key, nonce, timestamp))
	if err != nil {
		return false
	}

	receivedBytes, err := hex.DecodeString(receivedHMAC)
	if err != nil {
		return false
	}

	if !hmac.Equal(expectedBytes, receivedBytes) {
		return false
	}

	now := time.Now().Unix()
	if timestamp < now-allowedTimestampDiff || timestamp > now+allowedTimestampDiff {
		return false
	}

	if usedNonces[nonce] {
		return false
	}
	usedNonces[nonce] = true

	return true
}

How It Works

  1. generateMAC signs a canonical string containing the message, timestamp, and nonce.
  2. verifyHMAC recomputes the signature and compares it with hmac.Equal, which avoids ordinary string-comparison timing behavior.
  3. The timestamp must fall within an allowed time window.
  4. The nonce must not have been accepted previously.

Why Include a Nonce and Timestamp?

A valid HMAC alone does not stop an attacker from replaying a previously captured signed request.

  • A timestamp limits how long a signed request remains acceptable.
  • A nonce or unique request ID lets the receiver reject a request that has already been processed.

Production Considerations

The in-memory map[string]bool in this example is only suitable for demonstrating the idea. A real multi-process or multi-server application needs shared replay protection, usually with a datastore that can record nonces with an expiration time.

You should also define the signed message format carefully. If a request includes method, path, body, or headers, serialize those fields deterministically so both sides calculate the HMAC over exactly the same bytes.

Finally, store the shared secret in a secret manager or protected runtime configuration rather than in source code.

Conclusion

Go’s crypto/hmac package makes message authentication straightforward. HMAC verifies integrity and authenticity, while a timestamp and one-time nonce can add replay protection when they are validated and stored correctly.

Related Posts

Encrypt and Decrypt Data in Go with AES-GCM
Encrypt and Decrypt Data in Go with AES-GCM
Applications that store tokens, private configuration, or other sensitive values often need encryption at rest. Go includes everything required to implement modern symmetric encryption in its standard library. This example uses AES-GCM, an authenticated encryption mode that protects both confidentiality and integrity. That makes it a better default for new applications than older unauthenticated modes such as CFB. AES-GCM in Brief Symmetric encryption uses the same secret key for encryption and decryption. AES accepts 16-, 24-, or 32-byte keys for AES-128, AES-192, or AES-256. GCM adds authentication, so modified ciphertext is rejected during decryption. A fresh nonce must be used for every encryption operation with the same key. Complete Go Example Copy package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "fmt" ) func generateRandomKey() ([]byte, error) { key := make([]byte, 32) // AES-256 if _, err := rand.Read(key); err != nil { return nil, err } return key, nil } func encrypt(plaintext, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } // Prefix the ciphertext with the nonce so decrypt can recover it. return gcm.Seal(nonce, nonce, plaintext, nil), nil } func decrypt(ciphertext, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonceSize := gcm.NonceSize() if len(ciphertext) < nonceSize { return nil, fmt.Errorf("ciphertext too short") } nonce := ciphertext[:nonceSize] ciphertext = ciphertext[nonceSize:] return gcm.Open(nil, nonce, ciphertext, nil) } func main() { plaintext := []byte("Secret application data") key, err := generateRandomKey() if err != nil { panic(err) } ciphertext, err := encrypt(plaintext, key) if err != nil { panic(err) } fmt.Println("Ciphertext (base64):", base64.StdEncoding.EncodeToString(ciphertext)) decrypted, err := decrypt(ciphertext, key) if err != nil { panic(err) } fmt.Println("Decrypted text:", string(decrypted)) } How It Works generateRandomKey creates a cryptographically secure 32-byte key for AES-256. encrypt creates an AES cipher, wraps it with GCM, generates a random nonce, and encrypts the plaintext. The nonce is stored at the beginning of the returned byte slice. A nonce does not need to be secret, but it must not be reused with the same key. decrypt separates the nonce from the ciphertext and calls gcm.Open. If the ciphertext was modified, authentication fails and an error is returned. Key Management Matters The encryption code is only one part of a secure design. Do not hard-code real encryption keys in source control. In production, load keys from an appropriate secret-management system, protected environment, or key-management service.
Search Multiple Texts Concurrently with Goroutines in Go
Search Multiple Texts Concurrently with Goroutines in Go
Go makes concurrent work straightforward with goroutines. One practical example is searching for a word across many text values at the same time. Instead of checking each text sequentially, you can launch a goroutine for each item and wait for all searches to finish. Overview The program below will: Search for a word in a single text value with searchInText. Search multiple text values concurrently with searchWordInTexts. Print whether the word was found. Complete Example Copy package main import ( "fmt" "strings" "sync" ) // searchInText reports whether word occurs in text. func searchInText(text, word string) bool { if text == "" || word == "" { return false } // Make the search case-insensitive. text = strings.ToLower(text) word = strings.ToLower(word) return strings.Contains(text, word) } // searchWordInTexts searches multiple texts concurrently. func searchWordInTexts(texts []string, word string) bool { var wg sync.WaitGroup var mu sync.Mutex found := false for _, text := range texts { wg.Add(1) go func(t string) { defer wg.Done() if searchInText(t, word) { mu.Lock() found = true mu.Unlock() } }(text) } wg.Wait() return found } func main() { texts := []string{ "This is a long example text", "Another text for word searching", "This program uses goroutines for searching", } word := "program" if searchWordInTexts(texts, word) { fmt.Printf("Word '%s' was found in at least one text\n", word) } else { fmt.Printf("Word '%s' was not found\n", word) } } How It Works searchInText converts both the text and search term to lowercase, then uses strings.Contains for a case-insensitive match. searchWordInTexts uses sync.WaitGroup to wait for every goroutine and sync.Mutex to protect the shared found variable. main provides sample data, runs the concurrent search, and prints the result. A Practical Note For short in-memory strings, launching one goroutine per string may be slower than a simple loop because goroutines and synchronization have overhead. This pattern becomes more useful when each task is expensive or blocking, such as reading files, calling services, or processing large independent inputs.
chevron-up