Skip to Content
Course content

153: HMAC with crypto/hmac

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Let's say we're building a system where our server sends a webhook notification to a partner's API whenever a payment is processed. We can't just send the payment ID and amount in plain JSON; otherwise, anyone who finds that endpoint could spoof a "Payment Successful" request to the partner's server and trick them into shipping a product for free.

The problem with a simple hash

My first instinct might be to just hash the payload using SHA-256 and send that hash along as a signature. I'll try that out quickly.

package main

import (
	"crypto/sha256"
	"fmt"
)

func main() {
	payload := "order_id=123&amount=49.99"
	hash := sha256.Sum256([]byte(payload))
	fmt.Printf("Signature: %x\n", hash)
}

Okay, that gives me a fingerprint. But here's the flaw: if an attacker intercepts the request, they can change the amount to 0.01, calculate the SHA-256 of that new string, and replace the signature. The receiver would see a valid hash and assume everything is fine. A hash is for integrity, but it isn't for authenticity. I need a shared secret.

Trying to glue a key onto the hash

I could just prepend a secret key to the data before hashing it. It seems intuitive: sha256(secret + data). Let's see how that looks.

secret := "my-super-secret-key"
payload := "order_id=123&amount=49.99"
combined := secret + payload
hash := sha256.Sum256([]byte(combined))

This feels better, but this is actually a classic security mistake. Doing a simple concatenation makes the hash vulnerable to something called a "length extension attack." Depending on the hashing algorithm, an attacker can sometimes append more data to the end of the message and calculate a new valid hash without ever knowing the secret key. I don't want to be the guy who introduces a vulnerability because I tried to "roll my own" security logic.

Let's do this the right way with crypto/hmac

This is exactly why crypto/hmac exists. HMAC (Hash-based Message Authentication Code) is a specific construction that hashes the key and the message in a way that prevents those length extension attacks. It's the industry standard for this exact scenario.

Let's rewrite this. I'll use SHA-256 as the underlying hash function, but wrapped in the HMAC package.

package main

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

func main() {
	secret := []byte("my-super-secret-key")
	payload := []byte("order_id=123&amount=49.99")

	// hmac.New takes the hash function constructor and the key
	h := hmac.New(sha256.New, secret)
	
	// We write the data just like we do with a normal hash.Hash
	h.Write(payload)
	
	// h.Sum(nil) returns the final HMAC slice
	signature := h.Sum(nil)
	
	fmt.Printf("HMAC Signature: %s\n", hex.EncodeToString(signature))
}

Notice how hmac.New doesn't take a hash value, but the sha256.New function itself. This allows the HMAC package to manage the internal padding and double-hashing process that makes it secure. I just treat it like any other hash.Hash object: write the data, then call Sum.

The subtle danger of string comparison

Now, when the partner server receives this, they'll generate their own HMAC using the same secret and compare it to the one I sent. You might be tempted to just use if sigA == sigB or bytes.Equal(sigA, sigB). Stop right there.

Standard comparison operators return false as soon as they find the first differing byte. This means a request with a signature that is "almost" correct takes slightly longer to process than one that is completely wrong. An attacker can use a high-resolution timer to guess the signature byte-by-byte. This is a "timing attack."

To stop this, Go provides hmac.Equal, which performs a constant-time comparison regardless of whether the values match.

receivedSig, _ := hex.DecodeString(incomingHeader)
expectedSig := h.Sum(nil)

if hmac.Equal(receivedSig, expectedSig) {
	fmt.Println("Authenticity verified!")
} else {
	fmt.Println("Invalid signature!")
}

Always use hmac.Equal for cryptographic keys or signatures. It's a small change that closes a huge security hole.




📋 Practical Task

Build a Secure API Request Signer

You are tasked with creating a utility that signs API requests to prevent tampering. Create a program that does the following:

  • Defines a secret key (as a byte slice) and a message containing a JSON-like string (e.g., {"user_id": 42, "action": "delete_account"}).
  • Implements a function GenerateSignature(message, key []byte) string that returns the HMAC-SHA256 signature encoded as a hexadecimal string.
  • Implements a function VerifySignature(message, key []byte, signature string) bool that decodes the provided hex signature and uses hmac.Equal to verify if it matches the generated signature for that message and key.
  • In main, demonstrate a successful verification and a failed verification (by altering one character in the message).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.