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
generateMACsigns a canonical string containing the message, timestamp, and nonce.verifyHMACrecomputes the signature and compares it withhmac.Equal, which avoids ordinary string-comparison timing behavior.- The timestamp must fall within an allowed time window.
- 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.