Skip to main content

Verifier Server

The TLSNotary Verifier Server is a Rust-based HTTP/WebSocket server that acts as the verifier party in Multi-Party Computation TLS (MPC-TLS). It validates cryptographic proofs generated by the browser extension without ever seeing the user's private data.

The verifier server additionally accepts sessions in proxy mode on the same deployment.

Why a Verifier Server?​

TLSNotary uses Multi-Party Computation (MPC) to generate cryptographic proofs of TLS data. This requires two parties:

  1. Prover (Browser Extension) — the user generating the proof
  2. Verifier (This Server) — an independent party that validates the proof

What the Verifier Does​

  • Participates in MPC-TLS handshake — co-signs the TLS handshake without seeing plaintext data
  • Validates transcript integrity — ensures the HTTP request/response hasn't been tampered with
  • Generates proof — creates cryptographic proof that data came from a genuine TLS connection
  • Forwards requests — acts as a WebSocket proxy to route TLS traffic to target servers
  • (Proxy mode) — alternatively, forwards TLS traffic via a session-bound WebSocket proxy and verifies a post-session ZK proof; see the proxy mode reference

What the Verifier Does NOT See​

  • User credentials — authentication tokens, cookies, and API keys remain encrypted
  • Hidden data — only data marked with action: 'REVEAL' in handlers is visible
  • Committed data — data marked with action: 'PEDERSEN' is sent as a hash commitment, not plaintext

Architecture​

Key components:

  1. Session Management — Thread-safe HashMap stores session configuration
  2. WebSocket Endpoints — /session, /verifier, and /proxy handle different protocol stages
  3. MPC-TLS Verification — Uses tlsn crate for cryptographic verification
  4. Built-in Proxy — WebSocket-to-TCP bridge forwards requests to target servers
  5. Webhook System — Fire-and-forget POST notifications with redacted transcripts

Built-in WebSocket Proxy​

Naming note

This WebSocket proxy is a transport bridge and is distinct from proxy mode. In proxy mode the same /proxy endpoint additionally accepts a sessionId query parameter that binds the forwarded TLS traffic to a verification session.

note

The recommended approach is to run a WebSocket-to-TCP proxy on the prover's device for maximum control and privacy. However, this requires setup effort from the user. For convenience, the verifier includes a built-in proxy that works out-of-the-box. Any WebSocket-to-TCP proxy can be used — you can run your own, use a third-party service, or use the verifier's built-in proxy.

The verifier includes a built-in proxy that eliminates the need for external proxy services.

Endpoint: ws[s]://<host>/proxy?token=<target-hostname>

EnvironmentURL
Localws://localhost:7047/proxy?token=api.x.com
Productionwss://demo.tlsnotary.org/proxy?token=api.x.com

The token parameter specifies the target server hostname. The proxy:

  • Parses hostname and port from the token parameter (defaults to port 443)
  • Establishes a TCP connection to the target server
  • Bridges bidirectionally: WebSocket messages ↔ TCP stream
  • Cleans up automatically when either side disconnects
  • Logs total bytes forwarded for debugging

API Endpoints​

Health Check​

GET /health

curl http://localhost:7047/health
# ok

Create Session​

WebSocket /session

Creates a new MPC-TLS verification session.

Message (JSON):

{
"maxRecvData": 16384,
"maxSentData": 4096,
"sessionData": {
"userId": "user_123",
"purpose": "twitter_verification"
}
}
FieldTypeDefaultDescription
maxRecvDatanumber16384Maximum bytes the prover can receive
maxSentDatanumber4096Maximum bytes the prover can send
sessionDataobject—Custom metadata, included in webhook notifications

Response:

{
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}

Session lifecycle:

  1. Extension opens WebSocket to /session and sends configuration
  2. Server generates UUID, stores config, returns sessionId
  3. Extension opens second WebSocket to /verifier?sessionId=<id>
  4. After verification completes, session is cleaned up

Verifier Connection​

WebSocket /verifier?sessionId=<session-id>

Connects to an existing session as the verifier party. Validates the sessionId, retrieves session config, spawns the MPC-TLS verification task, and returns redacted transcripts.

Sessions are single-use and automatically removed after the WebSocket closes.

WebSocket Proxy​

WebSocket /proxy?token=<host>

See Built-in WebSocket Proxy above.


Session Flow​

Complete flow for generating a TLS proof:


Webhook System​

The verifier can send proof data to external services via HTTP POST. This enables backend integration for proof verification, storage, or business logic.

Configuration​

Webhooks are configured in config.yaml:

webhooks:
# Per-server webhooks (matched by target hostname)
"api.x.com":
url: "https://your-backend.example.com/webhook/twitter"
headers:
Authorization: "Bearer your-secret-token"
X-Source: "tlsn-verifier"
Content-Type: "application/json"

"api.github.com":
url: "https://your-backend.example.com/webhook/github"
headers:
Authorization: "Bearer another-token"

# Wildcard: catch-all for any unmatched server
"*":
url: "https://your-backend.example.com/webhook/default"
headers:
X-Source: "tlsn-verifier"

Matching logic: exact hostname match first, then wildcard "*" fallback, then skip (no error).

Payload​

{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"sessionData": {
"userId": "user_123",
"requestId": "req_abc",
"purpose": "account_verification"
},
"server_name": "api.x.com",
"redactedTranscript": {
"sent": "<base64-encoded request with redactions>",
"recv": "<base64-encoded response with redactions>"
},
"revealConfig": [
{ "type": "SENT", "part": "START_LINE", "action": "REVEAL" },
{ "type": "RECV", "part": "BODY", "action": "REVEAL", "params": { "type": "json", "path": "screen_name" } }
]
}

The redactedTranscript contains only data marked as revealed in the handlers. Data with action: 'PEDERSEN' or unmarked data is not included.

Example Backend Handler​

// Express.js webhook endpoint
app.post('/webhook/twitter', async (req, res) => {
const { sessionId, sessionData, server_name, redactedTranscript, revealConfig } = req.body;

// Decode base64 transcripts
const sentData = Buffer.from(redactedTranscript.sent, 'base64').toString('utf-8');
const recvData = Buffer.from(redactedTranscript.recv, 'base64').toString('utf-8');

console.log('Session:', sessionId);
console.log('User ID:', sessionData.userId);
console.log('Revealed request:', sentData);
console.log('Revealed response:', recvData);

await db.proofs.insert({
sessionId,
userId: sessionData.userId,
server: server_name,
sentData,
recvData,
timestamp: new Date(),
});

res.status(200).json({ received: true });
});

Behavior​

Webhooks are fire-and-forget:

  • Sent asynchronously in a separate tokio::spawn task
  • Errors are logged but do not affect the proof result
  • No retry logic (single attempt)
  • Default HTTP client timeout (~30 seconds)

If the webhook endpoint is down, the proof still succeeds. Check verifier logs for webhook errors.


Deployment​

Local Development​

cd packages/verifier
cargo run
# Server starts on http://0.0.0.0:7047

Production Build​

cargo build --release
./target/release/tlsn-verifier-server

Docker​

For a complete Docker deployment example with docker-compose, see the official docker-compose.yml in the repository.

Quick start with docker-compose:

git clone https://github.com/tlsnotary/tlsn-extension.git
cd tlsn-extension/packages/demo
docker-compose up verifier
# Server starts on http://0.0.0.0:7047

Configuration​

Server Settings​

  • Host: 0.0.0.0 (all interfaces)
  • Port: 7047
  • Source: packages/verifier/src/main.rs

Webhook Configuration​

Location: packages/verifier/config.yaml

  • Server loads config.yaml from the current working directory
  • If not found, webhooks are disabled (no error)
  • Invalid YAML causes startup failure

CORS​

Default: permissive (allows all origins). For production, restrict to specific origins:

let cors = CorsLayer::new()
.allow_origin("https://demo.tlsnotary.org".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST])
.allow_headers(Any);

Logging​

Set via RUST_LOG environment variable:

RUST_LOG=info cargo run # Recommended for production
RUST_LOG=debug cargo run # Detailed logs
RUST_LOG=trace cargo run # Very verbose

Development​

Project Structure​

packages/verifier/
├── src/
│ ├── main.rs # HTTP server, routing, WebSocket handlers
│ ├── verifier.rs # MPC-TLS verification logic
│ ├── axum_websocket.rs # WebSocket-to-AsyncRead/Write bridge
│ └── tests/ # Integration tests
├── Cargo.toml
├── Cargo.lock
├── config.yaml # Webhook configuration
└── Dockerfile

Testing​

cargo test # Run all tests
cargo test -- --nocapture # With output
cargo test test_name # Specific test

Testing with the Extension​

  1. Start the verifier:

    cd packages/verifier
    RUST_LOG=debug cargo run
  2. Build and load the extension:

    cd packages/extension
    npm run dev
    # Load from packages/extension/build/ in chrome://extensions/
  3. Open the Developer Console (right-click extension icon), paste plugin code, and click Execute.

  4. Check verifier logs:

    [Session] New session created: 550e8400-...
    [Proxy] New proxy request for host: api.x.com
    [550e8400] Successfully proxied 1234 bytes
    Verification complete

Common Issues​

IssueSolution
Address already in use (os error 48)Kill existing process: lsof -ti:7047 | xargs kill -9
failed to load config.yamlCreate config.yaml or run from the directory containing it
WebSocket upgrade failsEnsure client sends Upgrade: websocket and Connection: Upgrade headers
Proxy connection timeoutCheck target server is reachable: nc -zv api.x.com 443