Skip to content

feat(parser_gateway): x402 gating, attestation verifier, module split - #449

Draft
pepe-anchor wants to merge 4 commits into
pepefigueira/prs-581-01-turnkey-envelopefrom
pepefigueira/prs-581-04b-gateway-x402
Draft

feat(parser_gateway): x402 gating, attestation verifier, module split#449
pepe-anchor wants to merge 4 commits into
pepefigueira/prs-581-01-turnkey-envelopefrom
pepefigueira/prs-581-04b-gateway-x402

Conversation

@pepe-anchor

Copy link
Copy Markdown
Contributor

Why

Host-side half of the x402 work, ported from the unmerged PR #304 onto current main. PR #304 is stacked on #295, which is closed, so its base is a dead branch and it sits 132 commits behind main. This PR brings the gateway part forward on its own so it can actually be reviewed.

Stacked on #446 rather than main: #446 moves the Turnkey envelope into host_primitives::turnkey, and building the module split on top of it avoids introducing a second copy of those types and the delete-vs-modify conflict that would follow.

What

  • Splits parser_gateway's single main.rs into modules: lib.rs, state.rs, auth.rs, attestation.rs, x402_config.rs, handlers/{mod,health,parse}.rs.
  • x402 price-tag configuration and middleware builder.
  • Optional shared-bearer-token gate on the gateway hop.
  • Demo TVC response-attestation verifier (verifies the enclave ephemeral-key signature against a pinned pubkey).
  • Moves this crate to axum 0.8. Not a new hazard: main already runs axum 0.8 in parser/cli alongside 0.6.20 in metrics, so two majors in the workspace is the existing status quo.

Deliberately left out, because they need host_primitives::payment_marker from #448 which is not in this base:

  • signing.rs (GatewaySigner) and bin/gateway_keygen.rs
  • handlers/parse_tvc.rs and bin/tvc_probe.rs (these also need the pivot's v2 route)
  • the dynamic "derive the 402 network from the parse-request chain" logic, which lives entirely inside parse_tvc.rs

Consequence to be explicit about: after this merges the gateway can do x402 verify and settle, but cannot yet sign a VerifiedPaymentMarker, so the trust pair is not complete until the follow-up PR that lands signing.rs.

Test evidence

cargo test -p parser_gateway  -> 47 passed, 0 failed (13 on main before this)
cargo fmt -- --check          -> clean
cargo clippy -p parser_gateway --all-targets -- -D warnings -> clean

The two boot-proof regression tests survive the module split and their mock constant values are byte-for-byte unchanged: mock_boot_proof_matches_production_wire_shape, error_response_carries_mock_boot_proof. Nothing in this PR's scope needs a live facilitator, network access, or an enclave, so no tests were skipped.

Rollback

Revert the commits. The gateway is used only in non-TEE local dev and CI, never wrapping a real enclave, so a revert cannot affect a production deployment.

Linear

PRS-581

Stacked on #446. Supersedes the gateway portion of #304.

🤖 Generated with Claude Code

pepe-anchor and others added 4 commits August 6, 2026 11:50
The gateway forwards parse responses to callers over the public
internet, but has no way to tell an unattested response from a real
one unless it checks the signature itself. Add a pinned-pubkey
verifier: every response's Signature.public_key must match a value
provisioned to the gateway at boot (TVC_DEMO_PINNED_PUBKEY_HEX or
_FILE), or the caller gets 502 instead of a forwarded response.

This is explicitly demo-only (see the module doc for the production
replacement sketch using the Turnkey SDK's Nitro attestation
verifier). It is a stopgap that lets the x402 settle-on-success
contract skip settlement for anything that didn't come from the
pinned enclave key, which is the property that matters until real
attestation lands.

Ported from the unmerged spec/x402-tvc-enforced branch (PR #304),
adapted for main's generated SignatureScheme, which has no from_i32
helper (only two variants exist, so the unsupported-scheme branch
just names the known one and falls back to the raw int).

Co-Authored-By: Claude <noreply@anthropic.com>
Cloud Run-style deploys that open the gateway to allUsers (so AI-agent
callers without gcloud identity tokens can reach it) need something to
keep random crawlers off the endpoint before the x402 settlement layer
runs. Add an Authorization: Bearer <token> gate that is off by default
(GATEWAY_AUTH_BEARER_TOKEN / _FILE both unset behaves exactly as
before) and, when configured, rejects everything except /health with
401 + WWW-Authenticate.

Constant-time compare via subtle::ConstantTimeEq, since a timing leak
on a shared secret defeats the point of having one. Whether this stays
once X-Stamp validation lands in the enclave (a later PR in this stack)
is an open question to settle in that PR's description; this is a
weaker, different trust boundary (public gateway vs. enclave ingress)
so keeping both is plausible defense in depth.

Ported from the unmerged spec/x402-tvc-enforced branch (PR #304, commit
9208e00); no changes needed, the module is self-contained.

Co-Authored-By: Claude <noreply@anthropic.com>
Env-driven config for the x402 (HTTP 402 Payment Required) handshake:
which facilitator to probe, which network/asset/payTo to advertise in
a 402, and a build_middleware() that turns the configured price tags
into an x402-axum X402LayerBuilder ready to wrap a route.

Three profiles (local/payai/custom) cover offline dev, the payai
facilitator, and bring-your-own config. The local profile seeds both
an EVM (base-sepolia) and a Solana (solana-devnet) tag with burn-style
payTo addresses so a single gateway answers either chain out of the
box with zero required env vars.

Ported from the unmerged spec/x402-tvc-enforced branch (PR #304) at
its final state; no changes needed beyond re-flowing a few comments to
ASCII. Note what did NOT come along: the branch's later per-request
network derivation (matching the 402's advertised network to the
parse request's chain) lives entirely in the hand-rolled TVC-enforced
handler (handlers/parse_tvc.rs), which is out of scope here because it
needs the gateway-signed payment-marker infra from a parallel PR. This
crate's v2 route stays on the static-middleware path: fixed price tags
for the life of the process, same as before this port.

Co-Authored-By: Claude <noreply@anthropic.com>
…g + attestation + bearer auth

main.rs had grown into one file holding the Turnkey envelope
construction, the health and parse handlers, and all of main()'s
wiring. Adding x402 gating, TVC attestation, and the bearer-token gate
on top of that would have made it worse. Split into state.rs
(AppState), handlers/{health,parse}.rs, plus the attestation/auth/
x402_config modules landed in the previous three commits, so main()
is now just: build the attestation verifier, build the router, probe
the x402 facilitator and mount /v2 behind its middleware if that
succeeds, layer the optional bearer-token gate, serve.

The mock boot-proof handling (MOCK_BOOT_PROOF_* constants,
mock_boot_proof(), and the error_response(msg) shim over
host_primitives::turnkey::error_response) moves into handlers/parse.rs,
since that's the only remaining consumer; the wire-shape tests
(mock_boot_proof_matches_production_wire_shape,
error_response_carries_mock_boot_proof) move with it unchanged, same
mock constant values, same assertions.

Bumps axum 0.6.20 -> 0.8 for this crate (parser/cli already runs 0.8,
so two axum majors in the workspace is the existing status quo) and
migrates main() off the removed axum::Server API onto
tokio::net::TcpListener + axum::serve.

v1 stays open; v2 is gated by x402-axum's middleware built from
x402_config, mounted only if the configured facilitator answers a
/supported probe at startup. Route logic, timeouts, and error handling
are otherwise unchanged from what main.rs already did.

Ported from the unmerged spec/x402-tvc-enforced branch (PR #304) at
its pre-VPM state (commits 8124e71, 7c14465, f323def), since the
later TVC-enforced handler needs the gateway-signed payment-marker
types that a parallel PR is still landing. Also carries bd3b065's
64 KiB public body cap.

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