Skip to main content
Developer menu
Partner previewv0

Signing requests

Every request the platform sends your model is signed with the platform’s Ed25519 key, and every callback you send back is signed with yours. Nobody shares a secret.

The scheme

The header is ShockAI-Signature: t=timestamp,kid=key id,ed25519=signature. The timestamp is Unix seconds, and the key id is the signing key’s RFC 7638 thumbprint. The signature is Ed25519, base64url without padding, over six lines joined by a newline: shockai-model-api-v0, the timestamp, the method, the target URL, the Idempotency-Key (an empty line when there is none) and the hex SHA-256 of the raw body.

The target URL is the origin in lowercase without a default port, then the path and query exactly as sent. It ties a request to your host and a callback to its run, and the timestamp ties it to a moment: reject any timestamp more than 300 seconds from your clock.

Keys

The platform’s public keys are JWKs at /developers/keys/v0.json. Load them when you start, pick the one whose thumbprint is the header’s kid, and fetch the file again when a request names a key you do not hold.

Your manifest declares your own public keys in signing.callbackKeys, and the platform pins them when your model is connected. Keep the private half on your host; the platform never asks for it. The templates print the block to pin on their first start, and refuse to start once their key file is lost rather than make a new key under the same version.

Verifying

Verify against the raw body bytes exactly as received, before parsing. Re-serialising the JSON changes the bytes and breaks the signature. The Python verifier needs the cryptography package.

import { createHash, createPublicKey, verify } from 'node:crypto';

export type Ed25519Jwk = { kty: 'OKP'; crv: 'Ed25519'; x: string };

/** 'unknown_key': fetch the platform's keys again, then check once more. */
export type SignatureCheck = 'ok' | 'malformed' | 'expired' | 'unknown_key' | 'mismatch';

/** The key id in the header: the key's RFC 7638 thumbprint. */
function thumbprint(jwk: Ed25519Jwk): string {
  const members = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x });
  return createHash('sha256').update(members).digest('base64url');
}

/**
 * Checks a ShockAI-Signature header. `target` is your public origin and the
 * request-target exactly as received, e.g. https://models.example.com/shockai/v0/runs.
 * `keys` are the platform's, from https://app.shockai.co/developers/keys/v0.json
 * (two during a rotation).
 */
export function verifyShockaiSignature(
  header: string | null,
  request: { method: string; target: string; idempotencyKey: string | null; body: string | Buffer },
  keys: Ed25519Jwk[],
  toleranceSeconds = 300,
  nowSeconds = Math.floor(Date.now() / 1000),
): SignatureCheck {
  const parts = (header ?? '').split(',').map((part) => part.trim());
  const field = (name: string) => {
    const values = parts.filter((part) => part.startsWith(`${name}=`)).map((part) => part.slice(name.length + 1));
    return values.length === 1 ? values[0]! : '';
  };
  const [timestamp, kid, signature] = [field('t'), field('kid'), field('ed25519')];

  if (!/^[0-9]{1,12}$/.test(timestamp) || !/^[A-Za-z0-9_-]{86}$/.test(signature)) return 'malformed';
  if (Math.abs(nowSeconds - Number(timestamp)) > toleranceSeconds) return 'expired';

  const jwk = keys.find((key) => thumbprint(key) === kid);
  if (!jwk) return 'unknown_key';

  const base = [
    'shockai-model-api-v0',
    timestamp,
    request.method.toUpperCase(),
    request.target,
    request.idempotencyKey ?? '',
    createHash('sha256').update(request.body).digest('hex'),
  ].join('\n');

  const valid = verify(
    null,
    Buffer.from(base),
    createPublicKey({ key: { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, format: 'jwk' }),
    Buffer.from(signature, 'base64url'),
  );
  return valid ? 'ok' : 'mismatch';
}

Rotating a key

The platform publishes its next key beside the current one before it switches, so trust every key in the file. To rotate yours, make the new key, publish a manifest version that lists both keys and reconnect, sign with the new key (a template reads it from HOST_SIGNING_KEY or data/host-key.pem), then drop the old one in the version after.

Test vector

The key is RFC 8032’s TEST 1, whose private half is public. Check your Ed25519 against the RFC first, then your signed string and header against these. Never sign anything real with this key.

Private key (RFC 8032 TEST 1)
9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
Public key (JWK)
{"kty":"OKP","crv":"Ed25519","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}
Key id
kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k
Timestamp
1790000000
Method
POST
Target URL
https://models.example.com/shockai/v0/runs
Idempotency-Key
run_7c4f2a
Body
{"specVersion":"0.1"}
Signed string
shockai-model-api-v0 1790000000 POST https://models.example.com/shockai/v0/runs run_7c4f2a 1700d07dca59ed2974e6c9aa0a00833336008879d076b51e9a28f984f7a30b79
Expected header
t=1790000000,kid=kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k,ed25519=icmmz4Se1SDqakdLPEA9iLxwTTtteH-fL6IL7msudlV3l-hU6cZ5TrpR0CzuLOBHZhSw9jTt1Wvdko17Gz0fCw
shell
# Recompute the test vector's signature with OpenSSL 3
cat > rfc8032-test1.pem <<'PEM'
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJ1hsZ3v/VpguoRK9JLsLMREScVpezJpGXA7rAMcrn9g
-----END PRIVATE KEY-----
PEM
body_hash=$(printf '%s' '{"specVersion":"0.1"}' | openssl dgst -sha256 -r | cut -d' ' -f1)
printf '%s\n' 'shockai-model-api-v0' '1790000000' 'POST' 'https://models.example.com/shockai/v0/runs' 'run_7c4f2a' > base.txt
printf '%s' "$body_hash" >> base.txt
openssl pkeyutl -sign -rawin -inkey rfc8032-test1.pem -in base.txt \
  | openssl base64 -A | tr '+/' '-_' | tr -d '='
# → icmmz4Se1SDqakdLPEA9iLxwTTtteH-fL6IL7msudlV3l-hU6cZ5TrpR0CzuLOBHZhSw9jTt1Wvdko17Gz0fCw