Vouch
Security

Request signing

The exact string that is signed, the headers that carry it, and the rules that reject a replay or a stale clock.

Attestation proves a device once. Signing proves that each later request came from that same device and has not been edited on the way.

The headers

HeaderValue
X-Vouch-Run-TokenThe token from the attestation exchange.
X-Vouch-TimestampUnix seconds.
X-Vouch-NonceAny value, used once.
X-Vouch-SignatureBase64url of HMAC-SHA256(run_token, canonical_string).

The canonical string

METHOD \n path \n hex(sha256(body)) \n timestamp \n nonce

Five lines joined with newlines. An empty body still contributes the hash of an empty string.

POST
/v1/sdk/opens
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
1789635600
3f9a1c4e8b2d6071

The path excludes the query string. Proxies reorder query parameters, and a signature that breaks when they do is a support ticket rather than a security feature.

What is rejected

ErrorWhen
signature_invalidThe signature does not match the request.
nonce_replayedThis nonce has already been used inside the window.
timestamp_skewThe timestamp is outside the skew window, five minutes by default.
run_token_expiredThe run token has passed its expiry.
run_token_invalidThe token is unknown or has been revoked.

All of them are 401. A request refused by policy rather than by signature is 403 with device_blocked.

Two details worth knowing. The signature is compared in constant time before the nonce is spent, so an attacker cannot burn somebody else's nonce with a wrong signature. And changing any part of the request, the method, the path, a single byte of the body, changes the hash and the signature with it.

Which endpoints require it

Signing is applied per endpoint pattern, so public endpoints stay unsigned. Your policies decide where it is required, matching on the request path.

Handling expiry

Run tokens are short lived by design. On a 401 carrying run_token_expired or run_token_invalid, attest again and retry the request once. The SDKs do this for you. Do not loop: if the second attempt also fails, something is wrong that retrying will not fix.

Signing by hand

The SDKs sign the HTTP clients they wrap. For anything else, compute the signature yourself. In Python:

import base64, hashlib, hmac, secrets, time

def sign(run_token: str, method: str, path: str, body: bytes) -> dict[str, str]:
    timestamp = str(int(time.time()))
    nonce = secrets.token_hex(8)
    canonical = "\n".join([
        method,
        path,                                   # no query string
        hashlib.sha256(body).hexdigest(),
        timestamp,
        nonce,
    ])
    digest = hmac.new(run_token.encode(), canonical.encode(), hashlib.sha256).digest()
    signature = base64.urlsafe_b64encode(digest).decode().rstrip("=")
    return {
        "X-Vouch-Run-Token": run_token,
        "X-Vouch-Timestamp": timestamp,
        "X-Vouch-Nonce": nonce,
        "X-Vouch-Signature": signature,
    }

The base64url encoding has its padding stripped.

On this page