Skip to content

feat(parser_http_server): HTTP+JSON pivot for TVC public ingress - #450

Draft
pepe-anchor wants to merge 1 commit into
pepefigueira/prs-581-01-turnkey-envelopefrom
pepefigueira/prs-581-03-pivot-v1
Draft

feat(parser_http_server): HTTP+JSON pivot for TVC public ingress#450
pepe-anchor wants to merge 1 commit into
pepefigueira/prs-581-01-turnkey-envelopefrom
pepefigueira/prs-581-03-pivot-v1

Conversation

@pepe-anchor

Copy link
Copy Markdown
Contributor

Why

Turnkey's TVC public ingress is HTTP only: Cloudflare in front of app-<uuid>.turnkey.cloud rejects gRPC with 403 (verified 2026-05-16). So switching the parse path onto the pivot needs a binary that speaks HTTP and JSON natively and calls parser_app::routes::parse in process, instead of gRPC over vsock.

This is the keystone of the PRS-581 stack. Two PRs are built directly on it in parallel (in-enclave X-Stamp auth, real NSM boot proof), so as much as it is a feature, it is the set of extension points those need.

What

Open v1 and v2 routes only, plus /health. No payment enforcement, no auth, no proto change in this PR.

The three seams it owns, and why each is shaped that way:

  1. Handlers take raw Bytes, never Json<T>. An X-Stamp signature covers the exact request bytes; letting serde deserialize and re-serialize changes them (key order, whitespace) and invalidates every signature. parse_envelope(&[u8]) deserializes while the caller keeps the untouched slice. A test pins this so a future refactor cannot quietly undo it.
  2. BootProofSource trait with a StaticBootProof implementation, so the NSM-backed one is a new file rather than a rewrite. awsAttestationDocB64 is an empty string here: empty, never faked, because a strict verifier must reject an unattested response outright.
  3. Manifest fields are borsh, not the JSON on disk. /qos.manifest holds JSON at qos rev 365ba7ed, but qosManifestB64 and qosManifestEnvelopeB64 are borsh bytes: the Go verifier borsh-deserializes both, and the attestation doc's user_data is sha256(borsh(manifest)). Base64-ing the file bytes would produce fields nothing can verify. Verified against visualsign-turnkeyclient manifest/parser.go.

Also threads include_intermediate_output through for parity with the gateway's REST shape (#414). Every response, success and error, carries bootProof.

Test evidence

cargo test -p parser_http_server -> 2 passed
  envelope_is_parsed_from_raw_bytes_not_reserialized
  static_boot_proof_has_the_six_keys_and_a_real_ephemeral_pubkey
cargo test -p integration --test http_server -> 1 passed
  health 200; v1 parse whose signature.publicKey matches the generated ephemeral key;
  bootProof with exactly the six production keys; v2 identical to v1;
  malformed body -> 400 WITH bootProof present
cargo build -p parser_http_server --features vsock -> compiles (production configuration)
make -C src build / test / fmt / lint -> clean

The integration test fails fast if the server dies before binding, polling try_wait alongside the port. wait_until_port_is_bound loops forever, so without that check a pivot built with --features vsock (which looks for the absolute in-enclave key path, panics at startup) would hang CI instead of failing it. Confirmed with a negative control: it now fails in 0.05s with a diagnostic naming the cause.

Rollback

Revert the commit. It adds a new binary that nothing calls yet and changes no existing behavior, so a revert is inert. Nothing is deployed by merging it.

Linear

PRS-581

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

🤖 Generated with Claude Code

Turnkey's TVC ingress is HTTP only (Cloudflare in front of
app-<uuid>.turnkey.cloud rejects gRPC with 403), so switching the parse
path onto the pivot needs a binary that speaks HTTP+JSON natively and
calls parser_app::routes::parse in-process.

Open v1 and v2 routes only. No payment enforcement, no auth, no proto
change: those are separate PRs on top. What this PR does own is the three
seams they plug into, so they can be written in parallel without
colliding: handlers take raw Bytes (an X-Stamp signature covers the exact
request bytes, and a Json<T> round-trip would invalidate it), bootProof
comes from a BootProofSource trait, and the manifest fields are already
borsh-encoded the way the Go verifier reads them.

The integration test fails fast if the server dies before binding. It
polls try_wait alongside the port, because wait_until_port_is_bound loops
forever: a pivot built with --features vsock looks for the absolute
in-enclave key path, exits at startup, and would otherwise hang CI
instead of failing it.

Co-Authored-By: Claude <noreply@anthropic.com>
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