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:
-
Construct a canonical request — a normalized representation of the HTTP method, path, headers, and body.
-
Compute an HMAC digest of the canonical request using a shared secret key.
-
Attach the signature to the request as a header.
-
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 (also used by AWS CLI, boto3) |
|
AWS secret key |
|
Default AWS region |
|
Temporary session token (STS) |
|
Generic HMAC signing key (non-AWS services) |
|
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.