DEVELOPERS · HMAC COOKBOOK

Sign a Platform Contract request in five languages.

Copy-paste implementations of signRequest + verifyRequest in Node.js, Python, Go, Ruby, and PHP. Constant-time compare, raw hex output, 5-minute clock-skew tolerance — the same logic Merkava Core uses on the other side of the wire.

The signing scheme

Every authenticated request carries two HTTP headers. Merkava Core sets them on outbound requests; your Drive verifies them on inbound. The scheme is identical in both directions.

Algorithm
HMAC-SHA256, raw hex output (no sha256= prefix, no Base64).
Signed payload
Exactly the string ${timestamp}:${path}. Path includes the query string. Method and body are NOT signed.
Timestamp
Unix milliseconds. Header X-Meridian-Timestamp. Skew tolerance is 5 minutes either side.
Signature
Header X-Meridian-Signature. Lowercase hex, 64 chars.
Comparison
Constant-time. Always check the lengths first — naive equality leaks bytes through timing.
Secret
One shared symmetric secret per Merkava deployment, distributed to approved developers as MERIDIAN_AGENT_SECRET. Server-side only.
What's NOT in the signature: HTTP method, body, headers other than the two listed above. If you're hashing the body, you're following a different spec.

Pick your language

Each tab is a complete, runnable implementation: signRequest for outbound calls and verifyRequest for the inbound middleware. Same five-minute skew window, same constant-time compare, same response shape.

No external dependencies — Node 18+ has everything in node:crypto.

'use strict';
const crypto = require('node:crypto');

const SKEW_TOLERANCE_MS = 5 * 60 * 1000;

function signRequest(path, secret, timestamp = Date.now()) {
  const payload = `${timestamp}:${path}`;
  const sig = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return { 'X-Meridian-Timestamp': String(timestamp), 'X-Meridian-Signature': sig };
}

function verifyRequest(path, headers, secret, now = Date.now()) {
  const ts = Number(headers['x-meridian-timestamp']);
  const sig = headers['x-meridian-signature'];
  if (!ts || !sig) return { ok: false, error: 'missing-headers' };
  if (Math.abs(now - ts) > SKEW_TOLERANCE_MS) return { ok: false, error: 'timestamp-skew' };
  const expected = crypto.createHmac('sha256', secret).update(`${ts}:${path}`).digest('hex');
  if (expected.length !== sig.length) return { ok: false, error: 'sig-length' };
  if (!crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'))) {
    return { ok: false, error: 'sig-mismatch' };
  }
  return { ok: true };
}

module.exports = { signRequest, verifyRequest };

Stdlib only — hmac, hashlib, time. Works on Python 3.8+.

import hmac
import hashlib
import time

SKEW_TOLERANCE_MS = 5 * 60 * 1000

def sign_request(path: str, secret: str, timestamp: int | None = None) -> dict:
    if timestamp is None:
        timestamp = int(time.time() * 1000)
    payload = f"{timestamp}:{path}".encode("utf-8")
    sig = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
    return {"X-Meridian-Timestamp": str(timestamp), "X-Meridian-Signature": sig}

def verify_request(path: str, headers: dict, secret: str, now_ms: int | None = None) -> dict:
    if now_ms is None:
        now_ms = int(time.time() * 1000)
    ts_raw = headers.get("x-meridian-timestamp") or headers.get("X-Meridian-Timestamp")
    sig = headers.get("x-meridian-signature") or headers.get("X-Meridian-Signature")
    if not ts_raw or not sig:
        return {"ok": False, "error": "missing-headers"}
    try:
        ts = int(ts_raw)
    except ValueError:
        return {"ok": False, "error": "timestamp-not-int"}
    if abs(now_ms - ts) > SKEW_TOLERANCE_MS:
        return {"ok": False, "error": "timestamp-skew"}
    payload = f"{ts}:{path}".encode("utf-8")
    expected = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return {"ok": False, "error": "sig-mismatch"}
    return {"ok": True}

Standard library only. Wire up VerifyMiddleware via net/http.

package meridian

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "errors"
    "fmt"
    "net/http"
    "strconv"
    "time"
)

const skewToleranceMs int64 = 5 * 60 * 1000

func SignRequest(path, secret string) http.Header {
    ts := time.Now().UnixMilli()
    payload := fmt.Sprintf("%d:%s", ts, path)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(payload))
    sig := hex.EncodeToString(mac.Sum(nil))
    h := http.Header{}
    h.Set("X-Meridian-Timestamp", strconv.FormatInt(ts, 10))
    h.Set("X-Meridian-Signature", sig)
    return h
}

func VerifyRequest(path string, headers http.Header, secret string) error {
    tsStr := headers.Get("X-Meridian-Timestamp")
    sigHex := headers.Get("X-Meridian-Signature")
    if tsStr == "" || sigHex == "" {
        return errors.New("missing-headers")
    }
    ts, err := strconv.ParseInt(tsStr, 10, 64)
    if err != nil {
        return errors.New("timestamp-not-int")
    }
    now := time.Now().UnixMilli()
    if abs64(now-ts) > skewToleranceMs {
        return errors.New("timestamp-skew")
    }
    sig, err := hex.DecodeString(sigHex)
    if err != nil {
        return errors.New("sig-not-hex")
    }
    payload := fmt.Sprintf("%d:%s", ts, path)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(payload))
    expected := mac.Sum(nil)
    if !hmac.Equal(expected, sig) {
        return errors.New("sig-mismatch")
    }
    return nil
}

func abs64(n int64) int64 {
    if n < 0 {
        return -n
    }
    return n
}

Stdlib only. Drop into a Rack middleware or a Rails controller filter.

require "openssl"

SKEW_TOLERANCE_MS = 5 * 60 * 1000

def sign_request(path, secret, timestamp: nil)
  timestamp ||= (Time.now.to_f * 1000).to_i
  payload = "#{timestamp}:#{path}"
  sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload)
  {
    "X-Meridian-Timestamp" => timestamp.to_s,
    "X-Meridian-Signature" => sig
  }
end

def verify_request(path, headers, secret, now_ms: nil)
  now_ms ||= (Time.now.to_f * 1000).to_i
  ts_raw = headers["x-meridian-timestamp"] || headers["X-Meridian-Timestamp"]
  sig    = headers["x-meridian-signature"] || headers["X-Meridian-Signature"]
  return { ok: false, error: "missing-headers" } if ts_raw.nil? || sig.nil?

  ts = Integer(ts_raw) rescue nil
  return { ok: false, error: "timestamp-not-int" } if ts.nil?
  return { ok: false, error: "timestamp-skew" } if (now_ms - ts).abs > SKEW_TOLERANCE_MS

  expected = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, "#{ts}:#{path}")
  return { ok: false, error: "sig-length" } if expected.bytesize != sig.bytesize

  # constant-time compare
  matched = expected.bytes.zip(sig.bytes).reduce(0) { |acc, (a, b)| acc | (a ^ b) }
  matched.zero? ? { ok: true } : { ok: false, error: "sig-mismatch" }
end

Pure PHP — hash_hmac + hash_equals for the constant-time compare.

<?php
declare(strict_types=1);

const SKEW_TOLERANCE_MS = 5 * 60 * 1000;

function signRequest(string $path, string $secret, ?int $timestamp = null): array {
    $timestamp = $timestamp ?? (int) round(microtime(true) * 1000);
    $payload = "{$timestamp}:{$path}";
    $sig = hash_hmac('sha256', $payload, $secret);
    return [
        'X-Meridian-Timestamp' => (string) $timestamp,
        'X-Meridian-Signature' => $sig,
    ];
}

function verifyRequest(string $path, array $headers, string $secret, ?int $nowMs = null): array {
    $nowMs = $nowMs ?? (int) round(microtime(true) * 1000);
    $tsRaw = $headers['x-meridian-timestamp'] ?? $headers['X-Meridian-Timestamp'] ?? null;
    $sig   = $headers['x-meridian-signature'] ?? $headers['X-Meridian-Signature'] ?? null;
    if ($tsRaw === null || $sig === null) {
        return ['ok' => false, 'error' => 'missing-headers'];
    }
    if (!ctype_digit((string) $tsRaw)) {
        return ['ok' => false, 'error' => 'timestamp-not-int'];
    }
    $ts = (int) $tsRaw;
    if (abs($nowMs - $ts) > SKEW_TOLERANCE_MS) {
        return ['ok' => false, 'error' => 'timestamp-skew'];
    }
    $expected = hash_hmac('sha256', "{$ts}:{$path}", $secret);
    if (!hash_equals($expected, $sig)) {
        return ['ok' => false, 'error' => 'sig-mismatch'];
    }
    return ['ok' => true];
}

Test your implementation

A round-trip test catches almost every implementation mistake. Sign a known payload with a known secret, then verify the same payload with the same secret — same library, same machine. If that fails, your verifier is broken before you even hit the network.

# Across every language above, this should produce the same hex output:
# Secret:    "shared-secret-do-not-leak"
# Timestamp: 1714248000000
# Path:      "/api/meridian/metrics?since=1714247000000"
# Expected:  "9d6c0b8c1cf6e2c8e5b8f3c8a7d1a3e8e3b9f1c7e8a1c6b3f7e9c1d5a3b8e7f3"

# Generate this output once with your sign function. Then verify the same
# inputs and confirm verifyRequest returns ok:true. If sign and verify agree,
# your library matches Merkava's. If they disagree, fix locally before
# pointing the manifest validator at your live URL.
The expected hex above is illustrative. Compute the real value with your library — the test is "sign and verify agree on a fixed input," not "match this exact byte string." (The byte string you generate locally must be identical across all five languages, though — that's the whole point of having one signing scheme.)
QUESTIONS

HMAC cookbook — questions, plainly answered.

What HMAC algorithm does the Merkava Platform Contract use?

HMAC-SHA256 with raw hex output. The signing input is the request method, path, body (or empty string), and an ISO-8601 timestamp, joined by newlines. Per-tenant 256-bit shared secret. Constant-time compare on verify is required to prevent timing-attack leaks.

How is the timestamp validated?

5-minute skew tolerance on either side of server time. Requests with timestamps outside that window get rejected with 401 — defends against replay attacks while tolerating reasonable clock drift between Drives and Core. The cookbook examples include the verify-side skew check.

Why constant-time compare?

Naïve string equality leaks signature info via timing — an attacker observes which signature bytes match by measuring response latency, brute-forces the rest. crypto.timingSafeEqual (Node), hmac.compare_digest (Python), hmac.Equal (Go), Rack::Utils.secure_compare (Ruby), hash_equals (PHP) all return in constant time regardless of input divergence point. Use them.

Does the order of header serialization matter?

Yes. The signing input is canonicalized: method + '\n' + path + '\n' + body + '\n' + timestamp, no headers in the signed payload. Signatures and timestamps go in X-Merkava-Signature and X-Merkava-Timestamp request headers; those headers are read by verify but aren't part of the signed input. The cookbook shows the exact concatenation order.

Which language should I pick for a new Drive?

Whatever your team writes well. The Platform Contract is language-agnostic — Quillsly is JavaScript, Centerline is TypeScript, but Python, Go, Ruby, and PHP all work fine. Test vectors are byte-for-byte identical across implementations; if your signRequest output matches the published vectors, your Drive will pass Core's signature verification.

How do I test signing correctness without deploying?

Run your signRequest output against the published test vectors at /resources/drive-hmac-test-vectors. Each vector lists method, path, body, timestamp, secret, and expected signature. If your implementation produces the same hex string for the same inputs, you're done. Catches the most common bugs: wrong digest algorithm, encoding mismatches, off-by-one in the canonicalization.

What if my signing keys leak?

Rotate immediately from Merkava: Settings → Drives → [Drive name] → Rotate signing key. Old key remains valid for a 24-hour overlap window so traffic in flight continues to verify; after the window only the new key is honored. Drives need to handle the overlap cleanly — accept either signature during rotation. The published SDK examples handle this automatically.

What's next