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
# 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