Help us shape the future of Go! Take the 2026 Go Developer Survey

spt-txn-pep

module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: Apache-2.0

README

SPT-Txn — enforcement points

Zero dependencies. Chain-agnostic. One Wrap().

Per-transaction authorization and a tamper-evident audit trail for any HTTP resource server or MCP tool server. go.mod has no require block: this module depends on nothing but the Go standard library, and it is going to stay that way.

pep, err := gateway.NewPEP(gateway.PEP{
    Name:     "my-resource-server", // identifies this PEP in every receipt
    Evidence: myEmitter,            // or evidence.None{} to opt out, explicitly
    /* Allowlist, Policy, Spend, Log, RKey, Requirements */
})
if err != nil {
    return err // refuses to build without an evidence emitter
}
http.ListenAndServe(":8080", pep.Wrap(yourHandler))

Construct through NewPEP, never as a bare struct literal: the literal skips the checks and leaves Evidence nil, and the first authorized request then panics. The constructor is where "a PEP without evidence is not a PEP" is enforced.

Every request is authorized against a declared transaction, a signed Transaction Receipt and a transparency-log entry are recorded, and the request reaches the protected resource only on ALLOW.

Two artifacts, two questions

Every decision — including a denial — produces both:

  • A Transaction Receipt (evidence) records what authority governed this decision: PEP identity, decision class, rule path, policy hash, jurisdiction.
  • A transparency-log entry (translog) records where that decision sits in a hash-chained, inclusion-provable Merkle log (RFC 6962 tree hash; the published root additionally binds the record count).

They compose. Neither substitutes for the other.

The receipt is a seam rather than an implementation: evidence is an interface, a struct of strings and an explicit no-op — no crypto, no canonicalization, no imports at all. The reference implementation lives in spt-txn-poc/pkg/receipt, next to the verifier, because the canonicalizer is the primary bypass surface and issuer and verifier must be the same version by construction, not by convention. Depending on it from here would also drag a zero-knowledge-proof stack into the dependency graph of anyone who only wanted middleware. So the dependency points the other way: this module defines the interface, the engine implements it, and a deployment may supply its own.

Evidence is a precondition of the decision, not a side effect. An Emit error means "not durably recorded", and the PEP must answer DENY/unavailable rather than proceed. A PEP constructed without an Emitter is refused outright, and evidence.None — the deliberate "no receipts" choice — is detectable at runtime, so a deployment cannot quietly drift into non-conformance.

What's here

Package What it does
gate The decision core: offline intent binding, ALLOW / DENY_VIOLATION / DENY_UNAVAILABLE
evidence The Transaction Receipt seam — interface, draft-03 field set, detectable no-op. Zero imports.
translog Hash-chained records + Merkle transparency log (RFC 6962 tree hash, published root bound to the record count); concurrency-safe under per-request goroutines
gateway HTTP PEP — PEP.Wrap(http.Handler) middleware, plus a transparency-log HTTP service
mcpgate MCP PEP — the same decision core enforcing agent tool calls

Both enforcement points call the same gate. Neither holds policy logic of its own.

What the edge checks, and what it doesn't: docs/PRESENTATION-SEAM.md states plainly which properties this module enforces (allowlist, binding, single use, freshness, evidence) and which belong to the engine's eight-step verifier (issuer signature, delegation chain, holder binding, revocation) — plus exactly what connecting the two would require. Read it before assuming a presented token is cryptographically verified here; today it is not.

Gating tools/call is not gating MCP. mcpgate authorizes one tool-call you hand it. It does not see your JSON-RPC transport and cannot police it. If you gate tools/call with the Enforcer and forward every other method untouched, you have protected one method, not the server: resources/read returns resource contents, prompts/get returns prompts, and both go straight through while the receipts show a clean record of the method you did check.

Make the pass-through an allowlist — forward only the methods you have decided are reads, and deny everything else, including methods that did not exist when you wrote the list. A denylist is wrong by default for every method its author did not think of.

This is not hypothetical: a middleware built on this model shipped for eight weeks with exactly that shape before it was found and inverted on 2026-09-05.

Why this is a separate module

These packages were extracted from spt-txn-x402-solana, where adopting the middleware meant pulling solana-go and ~28 indirect dependencies — a blockchain SDK and a Mongo driver — into the dependency graph of anyone who only wanted per-transaction authorization.

SPT-Txn is chain-agnostic. This module makes that structural rather than asserted: the dependency direction runs one way, from chain-specific consumers to this module, never back. If anything here ever needs a chain, the claim is gone.

Licence and scope

Apache-2.0. Enforcement points are free — running one costs nothing, and it always will. What is commercial is operating a fleet of them and proving compliance with them: the control plane (issuer registry, revocation distribution, trust-snapshot hosting), receipt aggregation and retention, control mappings and compliance packs.

Security features are never gated. Verifying a status list and pinning a trust snapshot are free; hosting and distributing them at fleet scale is not.

Specification

draft-coetzee-oauth-spt-txn-tokens-03 — https://datatracker.ietf.org/doc/draft-coetzee-oauth-spt-txn-tokens/

Datatracker is the single source of truth for the draft version.

Reference implementation: https://github.com/rudizee007/spt-txn-poc

Transparency-log format

translog writes spt-txn/translog/v2. The published root is the RFC 6962 tree hash over the records, wrapped with the record count:

root = SHA-256( 0x02 ‖ uint64be(n) ‖ MTH(records) )

0x00 and 0x01 are RFC 6962's leaf and node tags; 0x02 is the wrapper's. The empty log is wrapped the same way (n = 0). The tree itself is RFC 6962's unbalanced split with no leaf padding. The count wrapper follows the construction in BIS Working Paper No. 1374, Verifiable official statistics: a blockchain-based approach (September 2026), §4.1, adapted to SHA-256 and a 64-bit count.

Inclusion proofs are ordinary RFC 6962 audit paths; the wrapper is applied once, at the top. A verifier recomputes the tree hash from the leaf and its path, wraps it with the tree size, and compares — translog.VerifyInclusion does exactly that. The wrapper binds the size only relative to a root you already trust (an on-chain anchor, or another authenticated source). A root and a size accepted from the same untrusted party bind nothing.

Log files written as spt-txn/translog/v1 (this module up to v0.5.x) are refused with ErrFormat. A v1 root is the bare tree hash, which translog/kat/logentry_ref.py still prints as MERKLE_INTERNAL, so a root anchored from a v1 log stays reproducible.

Testing

go test -race ./...
scripts/mutate-merkle-size-binding.sh   # every mutation must be killed

Known-answer values come from an independent Python reference, translog/kat/logentry_ref.py — never from the Go code under test.

Each fuzz target's seed corpus runs under plain go test. To explore one, pass -fuzz, for example go test ./translog/ -run '^$' -fuzz '^FuzzReadLog_AcceptsOnlySoundLogs$' -fuzztime 5m.

Package Fuzz target Property
translog FuzzVerifyInclusion_BindsTheClaimedTreeSize A genuine proof verifies at the tree's size and at no other
translog FuzzVerifyInclusion_ReturnsForEveryClaimedSize Verification returns, and refuses, for any index, size and path length
translog FuzzVerifyInclusion_AcceptsOnlyProofsTheTreeGenerated Any proof that verifies is one the tree generates: the tree's size, the leaf at that index, that index's audit path
translog FuzzInclusionProof_EveryGeneratedProofVerifies Every generated proof verifies at the tree's size
translog FuzzReadLog_AcceptsOnlySoundLogs A file that loads verifies, proves every entry, and round-trips unchanged
gateway FuzzTransparencyEntry_ServesOnlyVerifiableProofs The entry endpoint never answers with a server error, and every proof it serves verifies independently

Status — read this before relying on it

Proof of concept. Not externally audited. Not for production use.

On the project's assurance ladder these packages sit at tested and adversarially reviewed; independent audit and production hardening are the next steps, and neither has happened. "Adversarially reviewed" here means AI-agent review plus the maintainer's own analysis — not an assessment by an independent security firm.

Contributing

Trust-boundary changes (the decision path, intent binding, receipt signing) are reviewed spec-first and adversarially before merge. Please open an issue describing the gap before sending a PR that touches gate, evidence or translog.

Do not add a third-party dependency to this module without discussion. The absence of one is a feature, not an oversight.

Directories

Path Synopsis
Package amount parses a caller-supplied decimal amount of a 6-decimal asset (USDC) into integer micro-units, without ever going through a float.
Package amount parses a caller-supplied decimal amount of a 6-decimal asset (USDC) into integer micro-units, without ever going through a float.
Package evidence defines how a PEP emits a spec Transaction Receipt.
Package evidence defines how a PEP emits a spec Transaction Receipt.
Package gate implements the SPT-Txn x402 payment gate: it decides ALLOW/DENY for an x402 payment BEFORE any transaction is signed.
Package gate implements the SPT-Txn x402 payment gate: it decides ALLOW/DENY for an x402 payment BEFORE any transaction is signed.
Package gateway is a drop-in x402 authorization enforcement point (PEP): wrap any http.Handler and it enforces the SPT-Txn decision on every request, emits a signed transparency-log entry, and forwards to the protected resource only on ALLOW.
Package gateway is a drop-in x402 authorization enforcement point (PEP): wrap any http.Handler and it enforces the SPT-Txn decision on every request, emits a signed transparency-log entry, and forwards to the protected resource only on ALLOW.
Package mcpgate enforces SPT-Txn authorization on AI-agent tool-calls — the MCP policy-enforcement-point profile.
Package mcpgate enforces SPT-Txn authorization on AI-agent tool-calls — the MCP policy-enforcement-point profile.
Package translog implements the SPT-Txn TRANSPARENCY LOG: a signed, hash-chained, fixed-width record per authorization decision, appended to an append-only log whose published Merkle root — an RFC 6962 tree hash wrapped with the record count — can be anchored on-chain.
Package translog implements the SPT-Txn TRANSPARENCY LOG: a signed, hash-chained, fixed-width record per authorization decision, appended to an append-only log whose published Merkle root — an RFC 6962 tree hash wrapped with the record count — can be anchored on-chain.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL