Skip to content

Signing & verification

sign: is an outbound field on a Phaze Transport action: the worker signs the response body, the browser verifies it before reading, and a tampered body throws. The verify is injected into the compiled call — the consumer imports only actions, hand-rolls no crypto, and (for native algorithms) ships zero crypto bytes.

getSession: newAction({
flatbuffer: SessionManifest,
sign: 'ed25519', // ← sign the response
handler: async ({ id }, { env }) => env.SESSIONS.get(id),
})

The signature is detached, in an X-Phaze-Sig header — the body stays a clean buffer (composes with flatbuffer:, zero-copy):

HTTP/2 200
content-type: application/x-flatbuf
x-phaze-sig: ed25519:<base64 signature>
<raw FlatBuffer bytes>

The worker signs the exact bytes the client receives (the FlatBuffer buffer, or the serialized JSON). The client reads X-Phaze-Sig, verifies it against the response bytes, and only then proceeds to the inline reads / JSON parse. On failure it throws → the async signal’s .error.

Signatures use one raw-hex key pair, the same format on both runtimes (the TS worker wraps the raw Ed25519 seed in the fixed PKCS8 prefix for Web Crypto; Rust’s phaze-crypto reads the same hex via from_hex):

  • PrivatePHAZE_SIGN_KEY, a worker secret (wrangler secret put). Signs.
  • PublicPUBLIC_PHAZE_SIGN_KEY, inlined into the client at build via phaze:env/client. Verifies.

The compiled verify embeds the 32-byte public key; rotation is a redeploy, no extra fetch.

Compile-time dispatch → per-algorithm emission

Section titled “Compile-time dispatch → per-algorithm emission”

The registry tells the compiler the algorithm for each action at build time. Given that, it picks the emission:

Algo (from the field)EmissionRuntime cost
ed25519, es256, aes-gcm (Web Crypto native)inline native verify/decryptsubtle.verify(...) inline, no module, no indirection
PQC ml-dsa / ml-kem (WASM)gated module pulled for that action onlyships only when the browser must do PQC
hybrid ed25519+ml-dsanative inline and WASM gated, both requirednative part stays inline

The classical rows (ed25519 / es256 / aes-gcm) ship today. The PQC sign: wiring ships on the Rust runtime: sign = "ed25519+ml-dsa" emits the dual X-Phaze-Sig and ml_dsa::verify_response_sig checks the ml-dsa half server-side (phaze::crypto::ml_dsa), runtime-proven by a live e2e (see Post-quantum & at-rest). The browser still verifies only the ed25519 half — 0 WASM.

So native algos compile to exactly the inline subtle.verify they’d be by hand. Zero runtime difference. The registry is purely a build-time decision — there’s no runtime algo-detection overhead (which would be the slow, all-verifiers-present way).

The field (sign: 'ed25519') is the signal; the compiler reads it and emits the native-inline path. The runtime X-Phaze-Sig: <algo>:<sig> header still carries the algorithm tag + signature (and, for rotation, a key id) so the emitted verifier checks the right bytes — but which verifier ships is decided at build. No runtime branching to “find the right algo,” no shipping every verifier just in case.

sign: 'ed25519+ml-dsa' requests a composite signature — two independent signatures over the body, both must verify (classical and post-quantum, defense-in-depth: a break in one doesn’t forge). The browser verifies the classical half natively for its live check; the post-quantum half is the long-lived assurance (see Post-quantum & at-rest).

The mirror of sign: is inbound verification — checking a signed request (a webhook from an untrusted sender). On the Rust runtime that’s the verify= attribute (Ed25519 / JWT); the TS verify: field is planned. Note the direction: sign: signs responses (the browser verifies); verify: checks requests (the server verifies). Opposite ends.

Client verification is identical whether a TS Phaze action or a Rust #[get] handler produced the response — both emit X-Phaze-Sig over the body, and the browser verifies with the inlined public key. Rust signs via phaze::crypto::ed25519 / phaze::crypto::p256; the browser via Web Crypto. One wire format, two runtimes.