API SECURITY · CRYPTOGRAPHY

HMAC Signature Verification for Public APIs

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:

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:

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.