Prove what a website told you.
This is a TLSNotary notary server and its clients. You fetch a page over HTTPS as you normally would; the notary takes part in the TLS session without seeing it and signs an attestation. You then show anyone the part you choose (one field of a JSON response, a single line of a page) with proof that the server really sent it.
The notary charges each session in Cashu ecash, so its users do not need an account.
You need to run the server yourself. There is no public notary, and the clients on this page do nothing without one. Setting one up takes a Linux server, a domain name and about an hour: see below.
How it works
-
Pay. Your client asks the notary for a ticket and gets HTTP
402with a Cashu payment request. It pays with an ecash token from one of the accepted mints and receives a ticket, good for 3 attempts within 30 minutes. - Connect. Your client opens the TLS connection to the website. The TLS keys are split between you and the notary by multi-party computation (MPC-TLS): only you can read the traffic, and you cannot forge it alone. Your client checks the server's certificate as a browser would.
- Attest. When the request is done, the notary signs an attestation: commitments to the encrypted transcript, the server's identity and the time. The notary learns neither the request, the response nor the site's name.
-
Present. From the attestation and your local secrets you build a presentation
that reveals only the bytes you choose. Everything else stays hidden and shows as
X. - Verify. Anyone who trusts the notary's key verifies the presentation offline: the server name and its certificate chain, the time, and the revealed bytes. No call to the notary or the website is needed.
The notary runs upstream TLSNotary tlsn (a pinned revision after v0.1.0-alpha.15) unchanged,
wrapped with payment, limits and an egress policy. Upstream no longer ships a notary server or runs a public
one; this is a notary you can run.
Run a notary
The server is tlsnc-notary, a single Rust binary behind nginx. It sells tickets, takes part in
the MPC-TLS sessions, signs attestations and keeps the ecash it earns in a wallet of its own. You need:
- a Linux server with systemd, nftables and nginx, and about 4 GB of RAM for the notary;
- a domain name and a TLS certificate for it;
- a machine with Rust ≥ 1.95 (and Node.js for the web app) to build it.
git clone https://github.com/jooray/tlsnotary-cashu && cd tlsnotary-cashu
cargo zigbuild --release -p tlsnc-notary --target x86_64-unknown-linux-gnu.2.28
npm install && deploy/build-web.sh
export TLSNC_HOST=root@server.example.org TLSNC_DOMAIN=notary.example.org
deploy/deploy.sh # user, config, key, systemd units, nftables, SELinux, nginx, web app
Then edit /etc/tlsnc/config.toml: the mints you accept, your prices, and the public addresses of
your own machines, which the notary must never connect to. The notary connects to hosts its clients name, so
it is fenced in three ways (its own address checks, systemd and nftables) and reaches only the public
internet on port 443. The full guide, with every step by hand, is
DEPLOY.md.
Your notary serves the web app at /app/ and publishes its key at
/.well-known/tlsnc-notary.json. Give your users its URL; every client below takes it as a
setting.
Notarize a page
Command line: tlsnc
The CLI connects to the website itself, so the notary never touches the target. It is the cheapest option and suits scripts and agents. It has a Cashu wallet built in.
# build it (Rust ≥ 1.95), or take a binary from the latest release
cargo install --locked --git https://github.com/jooray/tlsnotary-cashu tlsnc
export TLSNC_NOTARY=https://notary.example.org # your notary
tlsnc receive 500 # Lightning: prints an invoice (and a QR code), waits for payment
tlsnc receive cashuB… # or receive a Cashu token you already have
tlsnc balance
tlsnc notarize https://httpbin.org/json # pays the ticket, writes notarized.attestation + .secrets
tlsnc present --json slideshow.author # reveal one JSON field, writes notarized.presentation
tlsnc verify notarized.presentation # checks it against the notary's published keys
Installing step by step, including checking a release binary:
README.
Headers and bodies work like curl: -H 'Authorization: Bearer …', -X POST -d '…'.
Authorization, Cookie and similar headers are never revealed unless you pass
--allow-credentials. tlsnc backup prints the wallet's 12 words;
tlsnc restore "…" brings the funds back on another machine.
In the browser
The web app, served by your notary at /app/, runs the whole prover in WebAssembly in the
browser: enter a URL and optional headers (an API token, for example), paste a Cashu token, and download a
presentation. A web page cannot reach a website by raw TCP, so the browser's connection goes through the
notary's bridge, which forwards only encrypted TLS bytes. That counts as notary egress and costs more.
Pages you are logged in to need your cookies, which a web page cannot read. For those, use the browser
extension: it notarizes the tab you are on (with its cookies, if you allow it for that site) and carries its
own small Cashu wallet with balance, history and a 12-word backup. Download
tlsnc-extension.zip from the
latest release (or from your notary's
/dl/), unzip it, and load the folder in chrome://extensions with Developer mode on
(“Load unpacked”). Then enter your notary's URL in its settings.
Details.
From your own code
Rust: tlsnc-client (prover and presentations) and tlsnc-verify.
JavaScript: @tlsnc/client (pay, notarize, present in the browser) and
@tlsnc/verify (verification only, 600 KB of WebAssembly, works in Node and in any page).
See the README.
Paying with Cashu
Each notary sets its own prices, per session in sat, and shows them at /info. The defaults are
high on purpose: a notary should not be worth using as a cheap proxy, and a VPN should stay cheaper. Who
opens the connection to the website decides the column.
| Tier | Request | Response | You connect CLI, MPC | Notary connects browser, proxy |
|---|---|---|---|---|
| S | ≤ 4 KiB | ≤ 1 MiB | 42 | 210 |
| M | ≤ 16 KiB | ≤ 2 MiB | 100 | 500 |
| L | ≤ 32 KiB | ≤ 2 MiB | 200 | 1 000 |
The request limit is what you send (URL, headers, body); responses are cheap to notarize. A ticket allows 3 attempts: a failed session is retried with the same ticket, not refunded. Mint fees are on the payer.
Get ecash from one of the notary's mints (listed in its /info) with
tlsnc receive <amount> (pay the Lightning invoice it prints), or with any Cashu wallet
(Minibits, cashu.me, …) set to one of
those mints, then send yourself a token.
The protocol
Tickets are sold over plain HTTP following NUT-24
(HTTP 402 with an X-Cashu header carrying a
NUT-18 payment request):
POST /v1/tickets?tier=S&egress=client
→ 402 Payment Required
X-Cashu: creqA… # amount (net of mint fees), unit sat, accepted mints
POST /v1/tickets?tier=S&egress=client
X-Cashu: cashuB… # a token from one of those mints, at least the amount plus input fees
→ 200 {"ticket":"tk_…","expires_at":…,"attempts":3,"tier":{…},"egress":"client","received":42}
GET /v1/notarize # WebSocket; the ticket rides in the subprotocol header
Sec-WebSocket-Protocol: tlsnc, tlsnc.tk_…
Over that WebSocket runs an unmodified tlsn session, followed by one length-prefixed frame each way: the
attestation request and the signed attestation. A token that is worth too little is returned as a refund
token in the 400 response. POST /v1/tickets/status (body {"ticket":"tk_…"}) tells why a
session was refused. Full description: DESIGN.md.
Verifying
A presentation is only as good as the notary key you trust. Each notary publishes its keys at
/.well-known/tlsnc-notary.json; pin the key rather than fetching it each time.
Verify with tlsnc verify, with @tlsnc/verify in a page or in Node, or in the web app.
Verification checks the signature, the server certificate against the Mozilla roots, and that the revealed
bytes match the attested transcript.
Limits and privacy
- MPC-TLS needs a server that speaks TLS 1.2 (most do, alongside 1.3).
- The notary sees neither your request nor the response nor, with the CLI, the website's name. The browser bridge has to know the host it connects to; it forwards only encrypted bytes.
- The notary connects only to public internet addresses, port 443, a limited number of times per host per hour. It refuses private, local and its operator's own networks.
- Payments are ecash: the notary learns that a token was redeemed, not who paid. Tickets never appear in URLs; the web server's access log keeps IP addresses for abuse handling, without query strings.
- Keep the
.secretsfile private: anyone with it can build presentations of your session.