Request binding
A passport says who you are, but on its own a stolen passport could be replayed against any request the audience accepts. Request binding closes that gap by proving what you are doing: transcript-v1 is a canonical, signed transcript of the request, so a passport can only be used for the exact request it was bound to. A stolen passport becomes useless for anything but an identical, in-window replay — which replay protection then blocks too.
How it works
Section titled “How it works”- Lives in
pkg/proof(transcript.go). - The transcript binds: method, authority, path, query (normalized), selected headers (allow
list), nonce, body hash, audience,
route_id, passportjti, issued-at bucket, and key binding. - The proof is verified by the adapter/middleware before replay state is consumed.
- Deny reasons:
request_binding_mismatch— the request does not match the signed transcript (tampered path/nonce/route, etc.).invalid_request_proof— the proof signature is malformed or does not verify.
flowchart LR
REQ["method + authority + path + query<br/>+ headers + nonce + body hash<br/>+ audience + route_id + jti<br/>+ iat bucket + key binding"] --> CAN["Canonical transcript string"]
CAN --> SIG["Signature (proof)"]
SIG --> VER["Verifier recomputes transcript<br/>from the ACTUAL request"]
VER --> M{"match?"}
M -- yes --> OK["binding valid"]
M -- no --> NO["request_binding_mismatch"]
See binding allow and deny
Section titled “See binding allow and deny”The clearest end-to-end demonstration is the provider/gateway flow, which signs real requests and shows both allow and tamper-deny:
make demo-provider-gatewayIt proves, among other things:
- a broker-issued, transcript-v1-bound request allows;
- changing the path after signing →
request_binding_mismatch; - a corrupted proof →
invalid_request_proof.
You can also produce signed request headers yourself (no broker) with trustplane sign, which
can print a ready-to-run curl via --curl:
# Conceptual shape — sign a request and emit the headers/curl the adapter acceptstrustplane sign \ --private-key "$CLIENT_KEY" \ --method GET \ --path /orders \ --audience acme.demo.orders \ --route-id acme.demo.orders.read \ --curlSend the produced request to the adapter and it verifies; tamper with the path and it denies.
Check cross-language conformance
Section titled “Check cross-language conformance”transcript-v1 must produce the same canonical string and digest in every language, or proofs signed by a non-Go SDK would not verify. The conformance suite checks this:
make transcript-conformanceThis runs Go, JavaScript (scripts/conformance/transcript_v1_conformance.js), and Python
(scripts/conformance/transcript_v1_conformance.py) over shared vectors and asserts identical
output. It is part of the v0.1 acceptance gate.
What to notice
Section titled “What to notice”- The binding covers the body hash, so you cannot swap the payload after signing.
- It covers the
route_id, so a proof foracme.demo.orders.readcannot be reused for a different route. - It covers an issued-at bucket and nonce, which (together with replay consume) bound the reuse window tightly.
Next steps
Section titled “Next steps”- Replay protection — make each accepted passport usable exactly once.