Skip to content

feat(parser_http_server): validate X-Stamp in the enclave - #451

Draft
pepe-anchor wants to merge 1 commit into
pepefigueira/prs-581-03-pivot-v1from
pepefigueira/prs-581-05-xstamp-auth
Draft

feat(parser_http_server): validate X-Stamp in the enclave#451
pepe-anchor wants to merge 1 commit into
pepefigueira/prs-581-03-pivot-v1from
pepefigueira/prs-581-05-xstamp-auth

Conversation

@pepe-anchor

Copy link
Copy Markdown
Contributor

Why

Today Turnkey's gateway authenticates every caller: it validates X-Stamp against Turnkey's own DB before the request reaches us. When the pivot becomes the front door, we lose that, and the pivot currently has zero authentication (the runbook's smoke test is a plain curl).

This keeps the X-Stamp wire shape and moves validation into the enclave, against a pubkey allowlist pinned at deploy time.

What

  • stamp.rs: Allowlist, StampError, and verify(&HeaderMap, &[u8], &Allowlist). Header value is base64url-no-pad JSON {publicKey, signature, scheme}, publicKey hex compressed SEC1, signature hex DER, over the raw body. Both P256 and secp256k1 schemes.
  • Allowlist arrives via --allowed-stamp-pubkeys-hex / ALLOWED_STAMP_PUBKEYS_HEX, delivered through pivotArgs, the same mechanism that already pins the gateway signing pubkey. Rotation costs a redeploy but no rebuild and no new trust root.
  • Absent allowlist means the routes stay open, which is today's behavior, so this cannot break the existing deployment on merge.
  • Constant-time pubkey comparison via subtle. A timing signal here leaks which keys are allowlisted.
  • 401 responses carry bootProof like every other response, and the error text is deliberately coarse ("invalid or missing X-Stamp") so it cannot be used to probe the allowlist.

Test evidence

cargo test -p parser_http_server -> 7 passed (5 new stamp tests)
  accepts_a_stamp_from_an_allowlisted_key
  rejects_a_stamp_from_an_unlisted_key
  signature_is_checked_against_raw_bytes_not_reserialized_json
  rejects_missing_header_and_malformed_encodings
  rejects_an_unsupported_scheme
cargo test -p integration --test http_server -> 2 passed
  unstamped -> 401 with bootProof; stamped by a listed key -> 200; unlisted key -> 401
make -C src test / fmt / lint -> clean

Stamps in tests are produced by turnkey_api_key_stamper itself, so the real producer is exercised rather than our own re-implementation of it.

One thing worth flagging in review: the raw-bytes test initially passed for the wrong reason. This workspace builds serde_json with preserve_order (it pulls indexmap), so an alphabetically-ordered fixture round-trips byte-identical through Value and the test proved nothing. The fixture now differs by insignificant whitespace, which compact output always drops regardless of key ordering, so the test has real teeth.

Open questions before this merges

  • Blocking: does anything between client and pivot rewrite request bodies? The stamp signs the body, so if Cloudflare normalizes, recompresses, or re-chunks it, body-signed stamps cannot survive the hop and this design has to change. Needs a probe against the live app before merge.
  • The stamp covers the body only: no timestamp, no nonce, no method or path. It is replayable by design, which is acceptable for a stateless read-only parse. With x402, a replayed body plus its original VPM is a free re-parse, but the VPM commits to request_hash, so it cannot be redirected at a different transaction.
  • What we give up versus Turnkey's DB check: per-org and per-user identity, activity-level policy, instant revocation. Acceptable while the caller set is small and known.
  • Recommendation on the gateway's GATEWAY_AUTH_BEARER_TOKEN: keep it. The gateway is internet-facing and sees unauthenticated x402 discovery traffic before any stamp exists on a request. It is a different trust boundary from the enclave hop this PR protects, and it is already an optional flag, so there is no cost to keeping defense in depth.

Rollback

Revert the commit, or simply stop passing --allowed-stamp-pubkeys-hex: with no allowlist configured the routes behave exactly as before this PR.

Linear

PRS-581

Stacked on #450.

🤖 Generated with Claude Code

Turnkey's gateway authenticates callers against their DB and we lose that
when the pivot becomes the front door. Keep the X-Stamp wire shape,
move validation into the pivot against a pubkey allowlist pinned via
pivotArgs (same delivery as --gateway-signing-pubkey-hex, so rotation is
a redeploy and nothing new to build).

Verification runs against the raw body bytes. A Json<T> round-trip
re-serializes and changes them, so the seam PR 3 cut (handlers take
Bytes) is what makes this correct; there is a test that pins it.

What we give up versus the DB check: per-org identity, activity policy,
instant revocation. Acceptable while the caller set is small and known;
the signed-allowlist option is the follow-up if rotation gets painful.

Co-Authored-By: Claude <noreply@anthropic.com>
@pepe-anchor pepe-anchor added the CI label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant