DocumentationBuild. Deploy. Operate.
DocsQSCSCore HTTP API

HTTP API: Signed Request Format

On this page 11 sections

Wire-level reference for the per-request ed25519 identity gate.


Browser Reference Implementation

If you are enabling the identity gate on your own origin, you do not need to re-implement the wire format below in the browser. The same client shim used by spooksystems.net and spook.systems in production is packaged as a drop-in SDK:

github.com/SpookSystems/qscs-crypto, qscs-crypto.js (~12 KB) plus the matching qscs-substrate.wasm released as a tagged asset (v20260513c is the current build, sha256 4e82d26bbecdbba0dc972d2f704183b3d543f36636b941c7a23ad4dad9906bf2).

Host both files on your own origin, include the script tag before any application code that uses fetch, and every same-origin request will be signed and gated correctly. The WASM path defaults to /wasm/qscs-substrate.wasm and is overridable via window.QSCS_WASM_URL before the script tag. The rest of this page is the wire reference for non-browser clients or for re-implementing the signer in another language.

Required Request Headers

HeaderFormatDescription
X-QSCS-Client-Uuid32 lowercase hex charactersClient identity. Generated by the WASM substrate on first visit; persists across reloads via IndexedDB. Required on every authenticated request, including /auth/login.
X-QSCS-TsDecimal milliseconds since Unix epochRequest timestamp. Must fall within ±60,000 ms of the node clock; otherwise the request is rejected.
X-QSCS-Nonce32 lowercase hex charactersCryptographically random per-request value. The (UUID, nonce) tuple is recorded server-side and accepted at most once.
X-QSCS-SigBase64 of 64 raw bytes (88 chars including padding)Detached ed25519 signature of the canonical message under the client's private key.

Canonical Message

The bytes signed by X-QSCS-Sig are produced by concatenating, in order:

METHOD     uppercase ASCII (e.g. GET, POST)
"
"       single LF byte
HOST       value of the HTTP Host header, without port for 80/443
"
"
URI        request-target including query string
"
"
BODY       raw request body (empty string for GET)
"
"
TS_MS      decimal string matching X-QSCS-Ts
"
"
NONCE_HEX  32-char hex string matching X-QSCS-Nonce

No trailing newline. No URL-encoding of the URI. No JSON re-canonicalisation of the body. The bytes the client signs and the bytes the server reconstructs must be byte-identical.

Login Body

The POST to /auth/login carries:

{
  "username":    "alice",
  "password":    "...",
  "client_uuid": "865c9393716f46a3bbd055b32bbcbf8c",
  "pubkey":      "<64 hex characters>"
}

The signature is verified against the supplied pubkey (proof that the requester holds the matching private key) before the credentials are forwarded to the control plane. On success, the node persists the (uuid, origin, pubkey) triple and replicates it to peers.

Subsequent Requests

Once logged in, the client signs every same-origin request with the same private key. The server looks up the stored pubkey for the supplied UUID and verifies. Requests with no signature, an unknown UUID, or a signature that fails to verify all return:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"error":"identity signature required"}

Replay Protection

  • Timestamps outside the ±60 s window are rejected.
  • Each (UUID, nonce) tuple is recorded in an in-memory LRU and rejected on second use.
  • Nonces older than the window are pruned automatically; the cache is bounded.

Logout

POST /auth/logout is signed identically. The server uses the stored pubkey for the supplied UUID to verify the signature, then erases the (uuid, origin) row. After a successful logout, all subsequent requests with that UUID fail.

Server-Injected Header

After a request passes signature verification, QSCSCore injects:

X-QSCS-Identity: <32-hex client UUID>

before forwarding to upstream PHP. Any client-supplied X-QSCS-Identity is stripped first; PHP can therefore trust the header unconditionally. The PHP helper requireIdentityAuth() reads this header and rejects requests that lack it, providing defence-in-depth in case of nginx misconfiguration.

Curl Example

For a manual integration test (note: you must construct the signature yourself; the standard curl invocation will not sign for you):

METHOD=POST
HOST=spooksystems.net
URI=/auth/login
BODY='{"username":"alice","password":"...","client_uuid":"...","pubkey":"..."}'
TS=$(date +%s%3N)
NONCE=$(openssl rand -hex 16)
MSG="${METHOD}
${HOST}
${URI}
${BODY}
${TS}
${NONCE}"
SIG=$(printf "$MSG" | ./ed25519-sign --key=privkey.bin | base64 -w0)

curl -X POST "https://${HOST}${URI}" 
  -H "Content-Type: application/json" 
  -H "X-QSCS-Client-Uuid: $UUID" 
  -H "X-QSCS-Ts: $TS" 
  -H "X-QSCS-Nonce: $NONCE" 
  -H "X-QSCS-Sig: $SIG" 
  -d "$BODY"

The browser substrate handles all of this automatically; the snippet above is provided only for those building non-browser clients against QSCS.

Common Failure Modes

Server log reasonCause
missing signature headersOne of the four X-QSCS-* headers is absent. Typically a client-side bootstrap error (WASM not loaded, fetch shim bypassed).
nonce lengthX-QSCS-Nonce is not exactly 32 hex characters.
ts not numeric / ts empty / ts overflowX-QSCS-Ts is malformed.
ts/nonce rejectedTimestamp outside window or nonce already used. Re-issue with a fresh nonce and current timestamp.
sig lengthDecoded signature is not 64 bytes. Check base64 padding.
signature mismatchSignature does not verify against the stored pubkey. Most commonly the canonical message bytes differ between client and server, check method case, host (no port), URI (raw, not re-encoded), and body (raw bytes, not re-serialised JSON).
Need a hand with your deployment?Contact support ↗Back to top ↑