Exposing REST or GraphQL APIs for public integration requires robust authentication protocols. While standard HTTP Bearer tokens (JWTs or API keys) confirm the sender's identity, they do not guarantee data integrity or protect against replay attacks. If a malicious actor intercepts an HTTP payload in transit, they can modify request attributes before forwarding the request to your backend.
To verify that an incoming request has not been tampered with and was generated by a trusted partner possessing a shared secret, industry leaders (such as Stripe, GitHub, and RiddleTech) mandate Hash-based Message Authentication Code (HMAC) SHA-256 signatures. In this technical deep dive, we explore HMAC mechanics, timing attack vulnerabilities, replay prevention, and middleware implementation.
1. How HMAC Signature Verification Works
HMAC combines a cryptographic hash function (SHA-256) with a pre-shared secret key. Before sending an HTTP POST request, the sender constructs a signature payload combining the HTTP method, request timestamp, request URI, and raw request body. The sender hashes this payload using their secret key and attaches the result to an HTTP header (e.g., X-Riddle-Signature).
Upon receiving the request, the server performs the exact same HMAC calculation on the raw body. If the calculated signature matches the incoming header signature, the server guarantees:
- Authentication: Only a client holding the shared secret key could generate the valid hash.
- Data Integrity: Changing a single byte in the request body invalidates the signature completely.
- Non-Repudiation: The client cannot deny generating the payload.
2. Timing Attacks & Constant-Time String Comparison
A critical security vulnerability in signature verification is the use of standard string equality operators (e.g. if incoming_sig == calculated_sig: or if (sigA === sigB)). Standard string comparison algorithms terminate execution at the first non-matching character.
An attacker can measure sub-microsecond response time variations over thousands of requests to guess valid signature characters one by one (a Timing Side-Channel Attack). To eliminate this vulnerability, verification code MUST use constant-time comparison algorithms (such as hmac.compare_digest() in Python or crypto.timingSafeEqual() in Node.js).
Security Rule: Never evaluate cryptographic signatures using standard string comparison operators. Always utilize constant-time comparison functions to prevent timing side-channel exploits.
3. Replay Attack Prevention via Timestamp Windows
If an attacker intercepts a valid signed HTTP payload, they can repeatedly send the exact same payload to your API endpoint (a Replay Attack), triggering duplicate transactions. To prevent replay attacks, require clients to include an epoch timestamp in the signature payload and header (e.g. X-Riddle-Timestamp: 1790432100).
The server enforces a strict time-drift tolerance window (typically ±300 seconds). If the incoming timestamp deviates further than 5 minutes from the server's system clock, the request is rejected immediately.
4. Production Python Middleware Implementation
Below is a production-grade HMAC verification middleware written for Python FastAPI / Starlette backends:
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
API_SHARED_SECRET = b"riddletech_live_secret_key_84920412"
TOLERANCE_SECONDS = 300 # 5-minute replay tolerance window
class HMACVerificationMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Exclude public health-check endpoints
if request.url.path in ["/health", "/docs", "/sitemap.xml"]:
return await call_next(request)
timestamp_header = request.headers.get("X-Riddle-Timestamp")
signature_header = request.headers.get("X-Riddle-Signature")
if not timestamp_header or not signature_header:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing required security headers"
)
# 1. Verify Timestamp Window to prevent Replay Attacks
try:
request_time = int(timestamp_header)
current_time = int(time.time())
if abs(current_time - request_time) > TOLERANCE_SECONDS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Request timestamp expired or out of tolerance window"
)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid timestamp format"
)
# 2. Read raw request body
raw_body = await request.body()
# 3. Construct canonical signature string (timestamp + method + path + body)
canonical_payload = f"{timestamp_header}.{request.method}.{request.url.path}.".encode("utf-8") + raw_body
# 4. Compute expected HMAC SHA-256 signature
expected_hash = hmac.new(
API_SHARED_SECRET,
canonical_payload,
hashlib.sha256
).hexdigest()
expected_signature = f"v1={expected_hash}"
# 5. Constant-Time Signature Comparison (Prevents Timing Attacks)
if not hmac.compare_digest(expected_signature, signature_header):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid HMAC Signature"
)
return await call_next(request)
5. Secret Key Rotation Strategy
Enterprise API gateways must support zero-downtime key rotation. When updating shared secrets:
- Maintain dual active keys (Primary Key
v1and Secondary Keyv2). - Accept incoming signatures matching either
v1orv2during a 14-day transition grace period. - Decommission legacy key
v1after all partner clients migrate tov2.
Conclusion
Enforcing HMAC SHA-256 signature verification guarantees payload authenticity, preserves data integrity, and protects public API endpoints against replay and timing attacks. Integrating constant-time comparison algorithms and timestamp validation establishes enterprise-grade security for API ecosystems.