# Delivery signing protocol

Source: https://stormedo.com/docs/reference/delivery-signatures/

Reference Stormedo's version 1 Ed25519 delivery token, claims, body digest, public-key discovery, verification limits, and shared test vectors.

Use the maintained [JavaScript](https://stormedo.com/docs/sdks/javascript/) or [Python](https://stormedo.com/docs/sdks/python/) verifier when possible. This reference describes the current version 1 wire format for implementations in other languages. Verification must finish before business processing.

## HTTP headers and token encoding

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:

```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.

## Protected header

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

## Claims

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

## Trusted public keys

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](https://stormedo.com/delivery-signature-fixtures.json) 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.

## Shared test vectors

Download [delivery-signature-fixtures.json](https://stormedo.com/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](https://stormedo.com/docs/how-to/receive-deliveries/) for the full handler lifecycle.
