HMAC Signatures

How HMAC Signing Works

HMAC (Hash-based Message Authentication Code) authentication does not send credentials. Instead, the client signs the request with a secret key, and the server recomputes the signature to verify it matches.

The process:

  1. Construct a canonical request — a normalized representation of the HTTP method, path, headers, and body.

  2. Compute an HMAC digest of the canonical request using a shared secret key.

  3. Attach the signature to the request as a header.

  4. The server performs the same computation and compares signatures.

If the signatures match, the server knows:

  • The client possesses the secret key (authentication)

  • The request was not tampered with in transit (integrity)

  • The request is recent (replay protection, when timestamps are included)

AWS SigV4: The Reference Implementation

AWS Signature Version 4 is the most widely deployed HMAC signing scheme. Understanding SigV4 teaches you the pattern used by every HMAC-based API.

The Four Steps

1. Canonical Request
   HTTPMethod + "\n"
   CanonicalURI + "\n"
   CanonicalQueryString + "\n"
   CanonicalHeaders + "\n"
   SignedHeaders + "\n"
   HexEncode(SHA256(payload))

2. String to Sign
   "AWS4-HMAC-SHA256" + "\n"
   Timestamp + "\n"
   Scope (date/region/service/aws4_request) + "\n"
   HexEncode(SHA256(CanonicalRequest))

3. Signing Key
   DateKey    = HMAC-SHA256("AWS4" + SecretKey, Date)
   RegionKey  = HMAC-SHA256(DateKey, Region)
   ServiceKey = HMAC-SHA256(RegionKey, Service)
   SigningKey  = HMAC-SHA256(ServiceKey, "aws4_request")

4. Signature
   HMAC-SHA256(SigningKey, StringToSign)

curl with AWS SigV4

curl 7.75+ has built-in SigV4 support:

# Built-in SigV4 (curl 7.75+)
curl -s \
  --aws-sigv4 "aws:amz:us-east-1:s3" \
  --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \
  -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \
  https://my-bucket.s3.us-east-1.amazonaws.com/

For older curl versions, use the AWS CLI or compute the signature manually.

Manual Signature Computation

#!/usr/bin/env bash
# Minimal SigV4 signing example (GET request, no query string, no payload)

set -euo pipefail

# Configuration
METHOD="GET"
SERVICE="s3"
REGION="us-east-1"
HOST="my-bucket.s3.us-east-1.amazonaws.com"
ENDPOINT="https://${HOST}/"

# Timestamps
AMZDATE=$(date -u +%Y%m%dT%H%M%SZ)
DATESTAMP=$(date -u +%Y%m%d)

# Step 1: Canonical request
CANONICAL_URI="/"
CANONICAL_QUERYSTRING=""
PAYLOAD_HASH=$(printf '' | sha256sum | awk '{print $1}')
CANONICAL_HEADERS="host:${HOST}\nx-amz-content-sha256:${PAYLOAD_HASH}\nx-amz-date:${AMZDATE}\n"
SIGNED_HEADERS="host;x-amz-content-sha256;x-amz-date"

CANONICAL_REQUEST="${METHOD}\n${CANONICAL_URI}\n${CANONICAL_QUERYSTRING}\n${CANONICAL_HEADERS}\n${SIGNED_HEADERS}\n${PAYLOAD_HASH}"
CANONICAL_REQUEST_HASH=$(printf "${CANONICAL_REQUEST}" | sha256sum | awk '{print $1}')

# Step 2: String to sign
CREDENTIAL_SCOPE="${DATESTAMP}/${REGION}/${SERVICE}/aws4_request"
STRING_TO_SIGN="AWS4-HMAC-SHA256\n${AMZDATE}\n${CREDENTIAL_SCOPE}\n${CANONICAL_REQUEST_HASH}"

# Step 3: Signing key (chained HMAC)
hmac_sha256() {
  printf '%s' "$2" | openssl dgst -sha256 -hmac "$1" -binary
}

hmac_sha256_hex() {
  printf '%s' "$2" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$1" -binary | xxd -p -c 256
}

DATE_KEY=$(printf '%s' "${DATESTAMP}" | openssl dgst -sha256 -hmac "AWS4${AWS_SECRET_ACCESS_KEY}" -binary | xxd -p -c 256)
REGION_KEY=$(hmac_sha256_hex "${DATE_KEY}" "${REGION}")
SERVICE_KEY=$(hmac_sha256_hex "${REGION_KEY}" "${SERVICE}")
SIGNING_KEY=$(hmac_sha256_hex "${SERVICE_KEY}" "aws4_request")

# Step 4: Signature
SIGNATURE=$(printf "${STRING_TO_SIGN}" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${SIGNING_KEY}" -binary | xxd -p -c 256)

# Step 5: Authorization header
AUTH_HEADER="AWS4-HMAC-SHA256 Credential=${AWS_ACCESS_KEY_ID}/${CREDENTIAL_SCOPE}, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}"

# Execute request
curl -s \
  -H "Authorization: ${AUTH_HEADER}" \
  -H "x-amz-date: ${AMZDATE}" \
  -H "x-amz-content-sha256: ${PAYLOAD_HASH}" \
  "${ENDPOINT}"

This is intentionally verbose to show every step. In practice, use curl’s --aws-sigv4 flag or the AWS CLI.

When HMAC Signing Is Used

Service Notes

AWS (all services)

SigV4 for every API call. The AWS SDK handles this transparently.

S3-compatible storage

MinIO, Wasabi, Backblaze B2 all accept SigV4 signatures.

Payment gateways

Some (not all) use HMAC to sign transaction requests.

Webhook verification

Services like GitHub, Stripe, and Slack sign webhook payloads with HMAC-SHA256 so you can verify authenticity.

Webhook Signature Verification

HMAC is used in reverse for webhooks — the sender signs the payload, and you verify it:

# GitHub webhook verification
# The webhook sends: X-Hub-Signature-256: sha256=<signature>

PAYLOAD='{"action":"push","ref":"refs/heads/main"}'
WEBHOOK_SECRET="your-webhook-secret"

# Compute expected signature
EXPECTED=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $NF}')

echo "sha256=${EXPECTED}"
# Compare with the X-Hub-Signature-256 header value

Shell Function: HMAC-SHA256

A reusable function for computing HMAC digests:

# HMAC-SHA256 with string key
hmac_sha256() {
  local key="$1"
  local message="$2"
  printf '%s' "$message" | openssl dgst -sha256 -hmac "$key" -hex | awk '{print $NF}'
}

# HMAC-SHA256 with hex key (for chained HMACs like SigV4)
hmac_sha256_hex() {
  local hex_key="$1"
  local message="$2"
  printf '%s' "$message" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${hex_key}" -hex | awk '{print $NF}'
}

# Usage
hmac_sha256 "my-secret-key" "message to sign"
# e.g., 4b393abced1c497f8048860ba1ede46a23f1ff5209b18e9c428bddfbb690aad8

dsec Pattern

For AWS-style authentication, store access keys:

dsec edit d000 dev/network

Inside the encrypted file:

AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_DEFAULT_REGION=us-east-1

For webhook verification, store the signing secret:

GITHUB_WEBHOOK_SECRET=your-webhook-secret
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx

Load before use:

dsource d000 dev/network

# AWS CLI reads these environment variables natively
aws s3 ls

# curl with SigV4
curl -s --aws-sigv4 "aws:amz:${AWS_DEFAULT_REGION}:s3" \
  --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \
  https://my-bucket.s3.amazonaws.com/

dsunsource

netapi Pattern

netapi handles HMAC computation when configured for AWS-style services:

# From environment (after dsource)
netapi --auth hmac aws s3 list-buckets

# Explicit flags
netapi --auth hmac \
  --access-key "${AWS_ACCESS_KEY_ID}" \
  --secret-key "${AWS_SECRET_ACCESS_KEY}" \
  --region us-east-1 \
  aws s3 list-objects --bucket my-bucket

Environment Variables

Variable Purpose

AWS_ACCESS_KEY_ID

AWS access key (also used by AWS CLI, boto3)

AWS_SECRET_ACCESS_KEY

AWS secret key

AWS_DEFAULT_REGION

Default AWS region

AWS_SESSION_TOKEN

Temporary session token (STS)

NETAPI_HMAC_KEY

Generic HMAC signing key (non-AWS services)

NETAPI_HMAC_ALGORITHM

Hash algorithm override (default: SHA-256)

netapi uses the AWS SDK’s credential resolution chain internally, so any method that works with the AWS CLI (environment variables, ~/.aws/credentials, instance profiles) also works with netapi.