Skip to content

Delivery signing protocol

Use the maintained JavaScript or Python verifier when possible. This reference describes the current version 1 wire format for implementations in other languages. Verification must finish before business processing.

Each delivery has exactly one Stormedo-Request-Id and one Stormedo-Signature. Reject missing, duplicated, or malformed values before parsing the token. The signature is a compact JWS with three base64url segments, without padding:

Plain text
base64url(protected JSON).base64url(claims JSON).base64url(Ed25519 signature)

Verify the Ed25519 signature over the ASCII bytes of the original first two segments joined by .. Do not parse and re-serialize them before signature verification. The token may contain at most 16 KiB; the request-ID header may contain at most 255 bytes. Reject malformed encodings and duplicate JSON member names.

Field Required value
alg EdDSA, using Ed25519.
typ stormedo-delivery+jwt.
kid Non-empty key ID matching a key in the configured project’s JWKS. Treat it as opaque.

The SDK rejects jku, jwk, x5u, x5c, crit, and b64 protected-header fields. Never fetch keys from a URL or embedded key supplied by the token. Do not allow the token to choose a different algorithm.

Claim Type Required interpretation
ver integer Exactly 1.
iss string Exactly the trusted issuer, https://api.stormedo.com for production.
sub string Exactly your configured prj_... project ID.
aud string Exactly the complete submitted destination URL, after Stormedo’s URL parsing. Includes path and query. No audience array.
jti string Exactly the single Stormedo-Request-Id header value.
method string Uppercase destination HTTP method. Compare to the received method uppercased.
iat integer Attempt issue time, Unix seconds.
nbf integer Not-before time. Current signer uses iat - 60.
exp integer Expiration time. Current signer uses iat + 600.
body_sha256 string SHA-256 of the exact destination body bytes, base64url without padding. Exactly 43 characters encoding 32 bytes.

All listed claims are required. Validate types strictly, including integer timestamps and version. The SDK applies 60 seconds of clock tolerance: reject iat or nbf later than now + 60; reject exp <= now - 60. Keep clocks synchronized and verify at ingress. These are short-lived delivery credentials, not stored authentication tokens.

The expected URL comes from trusted configuration. Supply the same canonical URL used to submit the request, including a trailing / if URL parsing adds one to a bare host. Do not derive it from arbitrary proxy headers. The verifier compares strings; it does not normalize away query differences.

Hash the original body bytes. For no body, hash the empty byte sequence. JSON whitespace and encoding affect the digest. Compare the decoded digest with the computed digest using a constant-time comparison.

Fetch https://api.stormedo.com/projects/{project_id}/delivery-signing-keys/jwks.json. It returns:

JSON
{
"keys": [
{"kty":"OKP","crv":"Ed25519","alg":"EdDSA","use":"sig","kid":"opaque-key-id","x":"base64url-public-key"}
]
}

This shape is illustrative; use the actual endpoint or test vectors for a valid key. x encodes a 32-byte public key. Match kid to a valid Ed25519 signing key. The endpoint is public and uses its own JWKS response, not Stormedo’s JSON envelope.

Honor freshness and ETag revalidation. Current responses allow 60 seconds of caching. Handle current and previous keys during rotation. An unknown key may trigger one refresh, subject to a 30-second cooldown in the SDK. Do not repeatedly fetch for arbitrary unknown key IDs or accept a key that is still absent after refresh.

Both SDKs bound JWKS responses to 64 KiB and eight keys and do not follow redirects. JavaScript enforces a five-second fetch deadline through response-body reading. Python uses HTTPX’s five-second inactivity timeout for each transport operation; a response that keeps sending chunks can take longer overall. Both require HTTPS except for loopback test servers. Custom key URLs cannot contain credentials, query strings, or fragments. Those overrides are for controlled tests, not values from delivery input.

Download delivery-signature-fixtures.json. The backend signer generates these fixtures using a public test-only root key. They cover JSON, the same business event with a fresh request ID, binary bytes, and an empty body.

For each case, base64-decode body_base64, use the supplied JWKS, and set a test clock to verified_at. Verify the signature and every expected binding. The historical tokens must fail against today’s clock. Never change a production clock or disable time checks to accept these fixtures.

Also check negative cases: alter one body byte, change the project/URL/method/request ID, corrupt the signature, omit or duplicate a header, and move the clock outside the allowed window. The SDK suites consume these same backend-produced fixtures.

The digest and signature do not prevent repeat processing of a valid request. Store the verified ID with the side effect atomically; use a stable business event ID for deduplication across replays. Follow Receive deliveries for the full handler lifecycle.