Skip to content

Wire contract

This is the authoritative specification every language port must reproduce. Each section below quotes the matching fixture in php/tests/vectors/ verbatim — a docs build step (scripts/check-vectors.mjs) fails if the JSON here ever drifts from the real vector files, so a porter can diff their output against these exact bytes and trust them.

Token layout

A token is a compact two-part string:

base64url(canonicalJson(payload)) . base64url(signature)
  • base64url is RFC 4648 §5 without padding (+-, /_, no =).
  • The signature covers the first part's bytes (the base64url payload string), not the raw JSON.
  • The payload carries v (version), type, kid (key id), nonce, iat, exp, and a data object.

Canonical JSON

The payload is serialized deterministically so every language produces identical bytes:

  • Object keys recursively sorted ascending by byte value.
  • Slashes and Unicode not escaped (PHP JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).
  • No insignificant whitespace.
  • Integer-like keys must keep string semantics — a port whose JSON encoder reorders or coerces numeric keys will not match; reject such payloads rather than silently reorder.

Signing

Default is HMAC-SHA256 (HS256); Ed25519 (EdDSA, detached) is opt-in.

HMAC-SHA256 (default)

json
{
  "description": "HMAC-SHA256 token round-trip. A port must produce this identical compact token from the same secret and payload (canonical JSON, sorted keys).",
  "secret": "vector-secret",
  "payload": {
    "v": 1,
    "type": "math",
    "kid": "k1",
    "nonce": "vgvectorsnonce000",
    "iat": 1000,
    "exp": 1120,
    "data": { "ah": "demo" }
  },
  "expectedCompact": "eyJkYXRhIjp7ImFoIjoiZGVtbyJ9LCJleHAiOjExMjAsImlhdCI6MTAwMCwia2lkIjoiazEiLCJub25jZSI6InZndmVjdG9yc25vbmNlMDAwIiwidHlwZSI6Im1hdGgiLCJ2IjoxfQ.hrkMQ2BPWAI3Z8fJp8XbsSLUtWfoVd0jI5hJDXTXJgo"
}

Ed25519 (opt-in, ext-sodium)

Deterministic signatures, so the compact token is stable. Detached signature over the canonical JSON payload.

json
{
  "description": "Ed25519 token round-trip (opt-in, ext-sodium). Keypair derived from a fixed 32-byte seed (all 0x01). Ed25519 signatures are deterministic, so the compact token is stable. Detached signature over the canonical JSON payload.",
  "enabled": true,
  "seedHex": "0101010101010101010101010101010101010101010101010101010101010101",
  "publicKeyHex": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
  "payload": {
    "v": 1,
    "type": "pow",
    "kid": "ed",
    "nonce": "n",
    "iat": 1,
    "exp": 2,
    "data": { "ah": "demo" }
  },
  "expectedCompact": "eyJkYXRhIjp7ImFoIjoiZGVtbyJ9LCJleHAiOjIsImlhdCI6MSwia2lkIjoiZWQiLCJub25jZSI6Im4iLCJ0eXBlIjoicG93IiwidiI6MX0.JAFWow6q2xYjk1t0iiBRG5_UXj7mXCm_SA-OMgvKlqGYhd2MWyqifkz0-e_GJF8X_xO1ht40SF7uvlFCeR9SDQ"
}

Proof-of-work

Default is PBKDF2-SHA256 Deterministic (the server plants a keySignature, the client brute-forces the counter); SHA-256 leading-zero-bit is opt-in. All binary payload fields are base64url of the raw bytes.

PBKDF2-SHA256 (default)

The port derives PBKDF2(nonce + counter, salt, cost) and base64url-encodes it; the result must equal keySignature. The smallest counter that matches is the answer.

json
{
  "description": "PBKDF2-SHA256 Deterministic PoW. A port derives PBKDF2(nonce+counter, salt, cost) and base64url-encodes it; the result must equal keySignature. keySignature is base64url of the raw derived key.",
  "challenge": {
    "algorithm": "PBKDF2-SHA256",
    "salt": "vsalt",
    "nonce": "vnonce",
    "cost": 100,
    "keySignature": "5BkpOKxye8efvkkqtqzTXTKULjSJdxLtfyJW2Q29w0E"
  },
  "correctCounter": 7
}

SHA-256 leading-zero-bit (opt-in)

Count the leading zero bits of SHA-256(salt . counter); the answer is the smallest counter whose digest has at least difficulty_bits leading zeros.

json
{
  "description": "SHA-256 leading-zero-bit (hashcash-style) PoW. A port must reproduce identical leading-zero-bit counting of SHA-256(salt . counter).",
  "challenge": { "algorithm": "SHA-256", "salt": "vsalt", "difficulty_bits": 4 },
  "correctCounter": 0
}

Single-use

Stateless by default; a Store makes a nonce one-shot (remembered on issue, consumed once on verify) so a solved token cannot be replayed.