Staylah

Core concepts

Request signing (HMAC)

The signed string, clock skew, replay protection, and a worked example.

Opt-in per key (requires_signature). Recommended for every push partner: a key alone proves you hold a secret; a signature also proves the body arrived unaltered and that this exact request has not been sent before. A replayed pull request costs a duplicate query; a replayed push costs a duplicate reservation.

Signed string#

Four fields, newline-joined, order fixed:

php-template
<timestamp>\n<METHOD>\n<path-with-query>\n<sha256-hex-of-raw-body>
  • timestamp — Unix seconds, the same value you send in X-Staylah-Timestamp.
  • METHOD — uppercase (GET, POST, PUT).
  • path-with-query — leading slash, no scheme or host (we sit behind a load balancer; signing a host you see and we don't makes every request fail irreproducibly). Include the query string when there is one.
  • body hash — sha256 hex of the raw body; empty string hashes to the sha256 of "".

Header value is the algorithm name, then =, then the HMAC hex:

css
X-Staylah-Signature: sha256=<hex_hmac_sha256(signed_string, signing_secret)>

The sha256= prefix is required so the algorithm can change later without guessing what an old client meant.

Rules#

  • Clock skew tolerance: 300 seconds by default. Outside it the request is rejected as stale. GET /ping returns our server clock so you can diagnose skew directly instead of guessing.
  • One use per signature, remembered for the tolerance window. Retrying a failed request means generating a new timestamp and signature — resending the identical signed request is treated as a replay.
  • A key flagged as requiring a signature but issued without a signing secret fails closed. It never degrades to "no signature needed".

Example (PHP)#

php
$timestamp = (string) time();
$body      = json_encode($payload, JSON_UNESCAPED_SLASHES);

$signed = implode("\n", [
    $timestamp,
    'PUT',
    '/api/connectivity/v1/ari',
    hash('sha256', $body),
]);

$signature = 'sha256=' . hash_hmac('sha256', $signed, $signingSecret);