Signature-Input and
Signature — in four languages. Every snippet below was executed against the hosted sandbox and
returns a real 2xx; see Verify your signer.
In production the
@openchildsafety/ocss SDK’s signRequest (TypeScript) and the
@phosra/gatekeeper SDK do this for you. The four-language recipe here is for
server-side signers in other stacks (Python/Go services, edge workers, CI) and for
understanding exactly what the wire looks like. Ed25519 signing keys are secret — keep them
server-side, never ship a seed to a browser or mobile client.The covered components
OCSS signs a closed, ordered set of components. Nothing else is covered, and the order is literal — the verifier reconstructs the base in exactly this sequence:| # | Component | Value |
|---|---|---|
| 1 | @method | the HTTP method, e.g. POST |
| 2 | @target-uri | the full absolute URL, e.g. https://…/api/v1/enforcement-endpoints |
| 3 | ocss-spec-version | the value of the OCSS-Spec-Version header — always OCSS-v1.0-pre |
| 4 | content-digest | only when the request has a body — the Content-Digest header value |
GET (a profile poll) covers components 1–3. Any request with a body (bind, rotate, confirm)
adds component 4. There are no other permutations.
The signature base
The base is built per RFC 9421 §2.5. For aPOST with a body it looks exactly like this (the \n are real newlines):
The exact bytes that get signed
Content-Digest—sha-256=:<standard-base64 of SHA-256(body)>:. The digest is over the exact request-body bytes you transmit. Serialize your JSON once, then hash and send those same bytes — if you re-serialize, the digest won’t match. (Omit this header entirely on aGET.)Signature-Input— the labelocss=followed by the same(...)params list used in the@signature-paramsbase line.createdis a Unix timestamp; the census rejects signatures skewed more than a few minutes, so use a monotonic clock, not a hard-coded value.Signature— the labelocss=wrapping the base64 signature between colons:ocss=:<sig>:.
Copy-paste signers
Pick your stack. All four produce byte-identical headers for the same inputs and all four are verified below against the live sandbox.The
curl + openssl path needs OpenSSL 3.0 or newer (the first release with raw Ed25519
pkeyutl -rawin). LibreSSL and OpenSSL 1.1 cannot sign Ed25519 from the command line — use the
Python, Go, or TypeScript signer there.Verify your signer
The fastest way to know your base is byte-correct is to sign a real request against the hosted sandbox and watch for a2xx. The sandbox emitter loopline signs with a deterministic test
seed (the slug bytes loopline right-padded with 0x01 to 32 bytes) — it is a test key, valid
only in the sandbox. Use it to smoke-test, then swap in your real Trust-List seed.
A 401 signature_invalid means the base you signed doesn’t match what the census rebuilt — check
the three traps above (component order, standard vs URL-safe base64, the trailing-newline rule).
A 403 means the signature verified but your DID isn’t accredited (or the consent-first gate
isn’t satisfied). The full self-serve loop is:
Mint test consent (no roster edit)
A signed
POST /sandbox/consent-attestations satisfies the §4.2.2 consent-first precondition
for a seeded sandbox child and returns its target_ref. See
Mint test consent.Bind
A signed
POST /enforcement-endpoints with that child_ref returns 201 and your
endpoint_id_label. See Bind an endpoint.Poll
A signed
GET /enforcement-profiles/{label} returns 200 with the router-signed profile.
See Fetch the signed profile.These signers are proven, not aspirational. The Python, Go, and
curl+openssl recipes on this
page were each run end-to-end against https://phosra-api-sandbox-production.up.railway.app and
returned the real 201/200 responses shown across the data-plane reference pages. If a snippet
here ever fails to verify, it is a bug in the docs — tell us.What the census does with it
On receipt the census:- Parses
Signature-Input, rebuilds the covered base in the fixed order, and re-hashes the body to checkContent-Digest. - Resolves
keyidto a public key on the OCSS Trust List and verifies the Ed25519 signature over the base. - Checks that your DID’s accreditation tier and the request’s band/scope are permitted, plus any endpoint-specific precondition (e.g. an active consent attestation for a bind).
401; trust/accreditation failures
are 403; neither leaks whether the underlying resource exists (§9.3(a) fail-closed). See
Errors for the full catalog.