mirror of
https://github.com/tlsnotary/tlsn-js.git
synced 2026-01-10 13:08:06 -05:00
Compare commits
29 Commits
v0.1.0-alp
...
inverted
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8358e57eab | ||
|
|
f7aa622dde | ||
|
|
cb5bf0c70c | ||
|
|
04010640f7 | ||
|
|
44fd63b0db | ||
|
|
8d5bd7257c | ||
|
|
8db907243e | ||
|
|
767425e5bf | ||
|
|
62dc1e3c74 | ||
|
|
be3ee16bb7 | ||
|
|
0914b907f8 | ||
|
|
3f585a60a1 | ||
|
|
055beb0ad1 | ||
|
|
6021db998c | ||
|
|
c5f752041b | ||
|
|
cbe14e39e4 | ||
|
|
ca3900bb07 | ||
|
|
fba5d08ac4 | ||
|
|
e6b7db5acf | ||
|
|
66ec4343e8 | ||
|
|
f51ddbf3de | ||
|
|
1cb664b341 | ||
|
|
4cecbb5334 | ||
|
|
8bc8a94948 | ||
|
|
8bf3745407 | ||
|
|
a418082762 | ||
|
|
07f2645a65 | ||
|
|
c70abc5eb2 | ||
|
|
be717f4260 |
48
.github/workflows/ci.yaml
vendored
48
.github/workflows/ci.yaml
vendored
@@ -1,30 +1,16 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
LOCAL-NOTARY: true
|
||||
LOCAL-WS: false
|
||||
HEADLESS: true
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RELEASE_MODE: 'dry-run' # dry-run by default, will be set to 'publish' for release builds
|
||||
services:
|
||||
notary-server:
|
||||
image: ghcr.io/tlsnotary/tlsn/notary-server:v0.1.0-alpha.9
|
||||
env:
|
||||
NOTARY_SERVER__TLS__ENABLED: false
|
||||
ports:
|
||||
- 7047:7047
|
||||
RELEASE_MODE: "dry-run" # dry-run by default, will be set to 'publish' for release builds
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -38,15 +24,15 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
|
||||
- name: Install stable nightly toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
components: rust-src
|
||||
toolchain: nightly
|
||||
targets: wasm32-unknown-unknown
|
||||
components: rust-src
|
||||
toolchain: nightly
|
||||
|
||||
- name: Use caching
|
||||
uses: Swatinem/rust-cache@v2.7.7
|
||||
@@ -59,21 +45,18 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Install Chrome
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
id: setup-chrome
|
||||
with:
|
||||
chrome-version: 121.0.6167.85
|
||||
|
||||
- name: Set CHROME_PATH environment variable
|
||||
run: echo "CHROME_PATH=${{ steps.setup-chrome.outputs['chrome-path'] }}" >> $GITHUB_ENV
|
||||
- name: install wstcp
|
||||
run: cargo install wstcp
|
||||
|
||||
- name: Install Chromium (Playwright)
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
@@ -94,4 +77,7 @@ jobs:
|
||||
if: env.RELEASE_MODE == 'publish'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc
|
||||
npm publish
|
||||
rm .npmrc
|
||||
|
||||
47
.github/workflows/playwright.yml
vendored
Normal file
47
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Tests demos
|
||||
on:
|
||||
pull_request:
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
name: Tests demos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: build tlsn-js
|
||||
run: npm ci; npm run build
|
||||
- name: install wstcp
|
||||
run: cargo install wstcp
|
||||
- name: Install Chromium (Playwright)
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Test react demo
|
||||
working-directory: demo/react-ts-webpack
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -e
|
||||
npm i
|
||||
npm run test
|
||||
- name: Test interactive verifier demo
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -e
|
||||
cd demo/interactive-demo/verifier-rs
|
||||
cargo build --release
|
||||
cd ../prover-ts
|
||||
npm i
|
||||
npm run test
|
||||
- name: Test web-to-web p2p demo
|
||||
working-directory: demo/react-ts-webpack
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -e
|
||||
npm run test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: "**/playwright-report/"
|
||||
retention-days: 30
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -6,4 +6,10 @@ dev-build/
|
||||
test-build/
|
||||
./demo/node_modules
|
||||
utils/tlsn
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"interactive-demo/verifier-rs/Cargo.toml",
|
||||
"interactive-demo/prover-rs/Cargo.toml"
|
||||
"demo/interactive-demo/verifier-rs/Cargo.toml",
|
||||
"demo/interactive-demo/prover-rs/Cargo.toml"
|
||||
],
|
||||
}
|
||||
109
CLAUDE.md
Normal file
109
CLAUDE.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Building and Development
|
||||
- `npm run build` - Build the library for production
|
||||
- `npm run build:lib` - Build both source and TypeScript definitions
|
||||
- `npm run build:src` - Build source with webpack
|
||||
- `npm run build:types` - Compile TypeScript definitions
|
||||
- `npm run build:wasm` - Build the WASM module (requires Rust and wasm-pack)
|
||||
- `npm run dev` - Start development server with file watching
|
||||
- `npm run watch:dev` - Watch mode for development builds
|
||||
|
||||
### Code Quality
|
||||
- `npm run lint` - Run both TypeScript checking and ESLint
|
||||
- `npm run lint:tsc` - TypeScript type checking
|
||||
- `npm run lint:eslint` - ESLint with auto-fix
|
||||
|
||||
### Testing
|
||||
- `npm test` - Run Playwright tests
|
||||
- `npx playwright test --ui` - Run tests with browser UI
|
||||
- `npx playwright test --debug` - Debug tests in browser
|
||||
- `npm run notary` - Start local notary server for testing
|
||||
|
||||
### Infrastructure
|
||||
- `npm run serve:test` - Serve test build on port 3001
|
||||
- `wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443` - WebSocket proxy for demos
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Library Structure
|
||||
- **src/lib.ts** - Main library exports including `Prover`, `Verifier`, `Presentation`, `Attestation`, `Secrets`, and `NotaryServer` classes
|
||||
- **src/types.ts** - TypeScript type definitions
|
||||
- **src/transcript.ts** - Transcript processing utilities
|
||||
- **src/utils.ts** - Utility functions for hex/array conversion and validation
|
||||
|
||||
### WASM Integration
|
||||
- Built around `tlsn-wasm` module (Rust-based WebAssembly)
|
||||
- WASM files are copied to build output via webpack configuration
|
||||
- Requires browser environment (Web Workers) - does NOT work in Node.js
|
||||
- Uses worker threads for cryptographic operations
|
||||
|
||||
### Key Classes and Their Roles
|
||||
|
||||
#### `Prover`
|
||||
- Main class for creating TLS proofs/attestations
|
||||
- `Prover.notarize()` - Static method for simple notarization workflow
|
||||
- Requires WebSocket proxy for TCP connections from browser
|
||||
- Supports client authentication and custom commit ranges
|
||||
|
||||
#### `Verifier`
|
||||
- Verifies proofs in interactive scenarios
|
||||
- Connects to prover instances for real-time verification
|
||||
|
||||
#### `Presentation`
|
||||
- Handles presentation of proofs with selective disclosure
|
||||
- Can be constructed from attestation/secrets or serialized data
|
||||
- Supports verification and transcript extraction
|
||||
|
||||
#### `NotaryServer`
|
||||
- Utility for interacting with notary servers
|
||||
- Handles session creation and key retrieval
|
||||
- Normalizes URLs between HTTP/HTTPS and WS/WSS protocols
|
||||
|
||||
### Demo Applications
|
||||
Three demo types showcase different usage patterns:
|
||||
- **react-ts-webpack** - React app with notary server attestation
|
||||
- **interactive-demo** - Real-time prover-verifier interaction
|
||||
- **web-to-web-p2p** - Peer-to-peer browser verification
|
||||
|
||||
### Build System
|
||||
- **webpack.build.config.js** - Production library build targeting `webworker`
|
||||
- **webpack.web.dev.config.js** - Development/test builds
|
||||
- Uses UMD format for broad compatibility
|
||||
- Copies WASM assets and snippets to build directory
|
||||
|
||||
### Testing Strategy
|
||||
- **Playwright** for browser-based testing (configured in playwright.config.ts)
|
||||
- Tests run against localhost:3001 with automatic build/serve
|
||||
- Separate test specs in both `/test/` (actual tests) and `/playwright-test/` (runners)
|
||||
- WebSocket proxy automatically started for tests
|
||||
- Tests require running notary server for full integration
|
||||
|
||||
### Dependencies and Environment
|
||||
- **Browser-only**: Uses Web Workers, WebSockets, and WASM
|
||||
- **Rust toolchain**: Required for WASM builds via tlsn-wasm submodule
|
||||
- **External services**: Requires WebSocket proxy (wstcp) and notary server
|
||||
- **Version**: Currently alpha.12 with specific tlsn-wasm dependency
|
||||
|
||||
### WebSocket Proxy Requirement
|
||||
Browsers cannot make raw TCP connections, so all demos require a WebSocket proxy:
|
||||
- Install: `cargo install wstcp` or `brew install wstcp`
|
||||
- Run: `wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443`
|
||||
- Used in all demos for connecting to target servers
|
||||
|
||||
### Development Workflow
|
||||
1. Build WASM if working on Rust code: `npm run build:wasm`
|
||||
2. Install dependencies: `npm install`
|
||||
3. For library development: `npm run dev` (watch mode)
|
||||
4. For testing: Start notary server, then `npm test`
|
||||
5. For demos: Navigate to demo directory and run `npm run dev`
|
||||
|
||||
### Important Notes
|
||||
- This library is specifically designed for the TLSNotary browser extension
|
||||
- API is not yet considered stable/public
|
||||
- All cryptographic operations happen in Web Workers for performance
|
||||
- Transcript data can be selectively revealed using commit/reveal patterns
|
||||
@@ -1,38 +0,0 @@
|
||||
# Test Rust Prover
|
||||
|
||||
1. Start the verifier:
|
||||
```bash
|
||||
cd verifier-rs; cargo run --release
|
||||
```
|
||||
2. Run the prover:
|
||||
```bash
|
||||
cd prover-rs; cargo run --release
|
||||
```
|
||||
|
||||
# Test Browser Prover
|
||||
1. Start the verifier:
|
||||
```bash
|
||||
cd verifier-rs; cargo run --release
|
||||
```
|
||||
2. Since a web browser doesn't have the ability to make TCP connection, we need to use a websocket proxy server to access <swapi.dev>.
|
||||
```bash
|
||||
cargo install wstcp
|
||||
|
||||
wstcp --bind-addr 127.0.0.1:55688 swapi.dev:443
|
||||
```
|
||||
3. Run the prover
|
||||
1. Build tlsn-js
|
||||
```bash
|
||||
cd ..
|
||||
npm i
|
||||
npm run build
|
||||
npm link
|
||||
```
|
||||
2. Build demo prover-ts
|
||||
```bash
|
||||
cd prover-ts
|
||||
npm i
|
||||
npm link
|
||||
npm run dev
|
||||
```
|
||||
3. Open <http://localhost:3456/> and click **Start Prover**
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "interactive-networked-prover"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-tungstenite = { version = "0.25", features = ["tokio-runtime"] }
|
||||
futures = "0.3"
|
||||
http = "1.1"
|
||||
http-body-util = "0.1"
|
||||
hyper = {version = "1.1", features = ["client", "http1"]}
|
||||
hyper-util = {version = "0.1", features = ["full"]}
|
||||
regex = "1.10.3"
|
||||
tokio = {version = "1", features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"net",
|
||||
"io-std",
|
||||
"fs",
|
||||
]}
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version ="0.3.18", features = ["env-filter"] }
|
||||
uuid = { version = "1.4.1", features = ["v4", "fast-rng"] }
|
||||
ws_stream_tungstenite = { version = "0.13", features = ["tokio_io"] }
|
||||
|
||||
tlsn-core = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.8", package = "tlsn-core" }
|
||||
tlsn-prover = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.8", package = "tlsn-prover" }
|
||||
tlsn-common = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.8", package = "tlsn-common" }
|
||||
@@ -1,9 +0,0 @@
|
||||
## Interactive Prover
|
||||
An implementation of the interactive prover in Rust.
|
||||
|
||||
## Running the prover
|
||||
1. Configure this prover setting via the global variables defined in [main.rs](./src/main.rs) — please ensure that the hardcoded `SERVER_URL` and `VERIFICATION_SESSION_ID` have the same values on the verifier side.
|
||||
2. Start the prover by running the following in a terminal at the root of this crate.
|
||||
```bash
|
||||
cargo run --release
|
||||
```
|
||||
@@ -1,172 +0,0 @@
|
||||
use async_tungstenite::{tokio::connect_async_with_config, tungstenite::protocol::WebSocketConfig};
|
||||
use http_body_util::Empty;
|
||||
use hyper::{body::Bytes, Request, StatusCode, Uri};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use regex::Regex;
|
||||
use tlsn_common::config::ProtocolConfig;
|
||||
use tlsn_core::transcript::Idx;
|
||||
use tlsn_prover::{state::Prove, Prover, ProverConfig};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::{debug, info};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
const TRACING_FILTER: &str = "INFO";
|
||||
|
||||
const VERIFIER_HOST: &str = "localhost";
|
||||
const VERIFIER_PORT: u16 = 9816;
|
||||
// Maximum number of bytes that can be sent from prover to server
|
||||
const MAX_SENT_DATA: usize = 1 << 12;
|
||||
// Maximum number of bytes that can be received by prover from server
|
||||
const MAX_RECV_DATA: usize = 1 << 14;
|
||||
|
||||
const SECRET: &str = "TLSNotary's private key 🤡";
|
||||
/// Make sure the following url's domain is the same as SERVER_DOMAIN on the verifier side
|
||||
const SERVER_URL: &str = "https://swapi.dev/api/people/1";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| TRACING_FILTER.into()))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
run_prover(VERIFIER_HOST, VERIFIER_PORT, SERVER_URL).await;
|
||||
}
|
||||
|
||||
async fn run_prover(verifier_host: &str, verifier_port: u16, server_uri: &str) {
|
||||
info!("Sending websocket request...");
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("ws://{}:{}/verify", verifier_host, verifier_port,))
|
||||
.header("Host", verifier_host)
|
||||
.header("Sec-WebSocket-Key", uuid::Uuid::new_v4().to_string())
|
||||
.header("Sec-WebSocket-Version", "13")
|
||||
.header("Connection", "Upgrade")
|
||||
.header("Upgrade", "Websocket")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
let (verifier_ws_stream, _) =
|
||||
connect_async_with_config(request, Some(WebSocketConfig::default()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!("Websocket connection established!");
|
||||
let verifier_ws_socket = WsStream::new(verifier_ws_stream);
|
||||
prover(verifier_ws_socket, server_uri).await;
|
||||
info!("Proving is successful!");
|
||||
}
|
||||
|
||||
async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(verifier_socket: T, uri: &str) {
|
||||
debug!("Starting proving...");
|
||||
|
||||
let uri = uri.parse::<Uri>().unwrap();
|
||||
assert_eq!(uri.scheme().unwrap().as_str(), "https");
|
||||
let server_domain = uri.authority().unwrap().host();
|
||||
let server_port = uri.port_u16().unwrap_or(443);
|
||||
|
||||
// Create prover and connect to verifier.
|
||||
//
|
||||
// Perform the setup phase with the verifier.
|
||||
let prover = Prover::new(
|
||||
ProverConfig::builder()
|
||||
.server_name(server_domain)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.setup(verifier_socket.compat())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Connect to TLS Server.
|
||||
let tls_client_socket = tokio::net::TcpStream::connect((server_domain, server_port))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Pass server connection into the prover.
|
||||
let (mpc_tls_connection, prover_fut) =
|
||||
prover.connect(tls_client_socket.compat()).await.unwrap();
|
||||
|
||||
// Wrap the connection in a TokioIo compatibility layer to use it with hyper.
|
||||
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
|
||||
|
||||
// Spawn the Prover to run in the background.
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// MPC-TLS Handshake.
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::spawn(connection);
|
||||
|
||||
// MPC-TLS: Send Request and wait for Response.
|
||||
info!("Send Request and wait for Response");
|
||||
let request = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.header("Host", server_domain)
|
||||
.header("Connection", "close")
|
||||
.header("Secret", SECRET)
|
||||
.method("GET")
|
||||
.body(Empty::<Bytes>::new())
|
||||
.unwrap();
|
||||
let response = request_sender.send_request(request).await.unwrap();
|
||||
|
||||
debug!("TLS response: {:?}", response);
|
||||
assert!(response.status() == StatusCode::OK);
|
||||
|
||||
// Create proof for the Verifier.
|
||||
let mut prover = prover_task.await.unwrap().unwrap().start_prove();
|
||||
|
||||
let idx_sent = redact_and_reveal_sent_data(&mut prover);
|
||||
let idx_recv = redact_and_reveal_received_data(&mut prover);
|
||||
|
||||
// Reveal parts of the transcript
|
||||
prover.prove_transcript(idx_sent, idx_recv).await.unwrap();
|
||||
|
||||
// Finalize.
|
||||
prover.finalize().await.unwrap()
|
||||
}
|
||||
|
||||
/// Redacts and reveals received data to the verifier.
|
||||
fn redact_and_reveal_received_data(prover: &mut Prover<Prove>) -> Idx {
|
||||
let recv_transcript = prover.transcript().received();
|
||||
let recv_transcript_len = recv_transcript.len();
|
||||
|
||||
// Get the homeworld from the received data.
|
||||
let received_string = String::from_utf8(recv_transcript.to_vec()).unwrap();
|
||||
debug!("Received data: {}", received_string);
|
||||
let re = Regex::new(r#""homeworld"\s?:\s?"(.*?)""#).unwrap();
|
||||
let homeworld_match = re.captures(&received_string).unwrap().get(1).unwrap();
|
||||
|
||||
// Reveal everything except for the homeworld.
|
||||
let start = homeworld_match.start();
|
||||
let end = homeworld_match.end();
|
||||
Idx::new([0..start, end..recv_transcript_len])
|
||||
}
|
||||
|
||||
/// Redacts and reveals sent data to the verifier.
|
||||
fn redact_and_reveal_sent_data(prover: &mut Prover<Prove>) -> Idx {
|
||||
let sent_transcript = prover.transcript().sent();
|
||||
let sent_transcript_len = sent_transcript.len();
|
||||
|
||||
let sent_string: String = String::from_utf8(sent_transcript.to_vec()).unwrap();
|
||||
let secret_start = sent_string.find(SECRET).unwrap();
|
||||
|
||||
debug!("Send data: {}", sent_string);
|
||||
|
||||
// Reveal everything except for the SECRET.
|
||||
Idx::new([
|
||||
0..secret_start,
|
||||
secret_start + SECRET.len()..sent_transcript_len,
|
||||
])
|
||||
}
|
||||
1
demo/interactive-demo/prover-ts/.gitignore
vendored
1
demo/interactive-demo/prover-ts/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
package-lock.json
|
||||
@@ -1,263 +0,0 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import * as Comlink from 'comlink';
|
||||
import { Watch } from 'react-loader-spinner';
|
||||
import { Prover as TProver } from 'tlsn-js';
|
||||
import { type Method } from 'tlsn-wasm';
|
||||
import './app.scss';
|
||||
import { HTTPParser } from 'http-parser-js';
|
||||
import { Commit, mapStringToRange, subtractRanges } from 'tlsn-js';
|
||||
|
||||
const { init, Prover }: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
|
||||
root.render(<App />);
|
||||
|
||||
const serverUrl = 'https://swapi.dev/api/people/1';
|
||||
// let websocketProxyUrl = 'wss://notary.pse.dev/proxy';
|
||||
const websocketProxyUrl = 'ws://localhost:55688';
|
||||
const verifierProxyUrl = 'ws://localhost:9816/verify';
|
||||
|
||||
function App(): ReactElement {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [result, setResult] = useState<string | null>(null);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
setProcessing(true);
|
||||
|
||||
const url = serverUrl;
|
||||
const method: Method = 'GET';
|
||||
const headers = {
|
||||
secret: "TLSNotary's private key",
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const body = {};
|
||||
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
let prover: TProver;
|
||||
try {
|
||||
console.time('setup');
|
||||
await init({ loggingLevel: 'Info' });
|
||||
console.log('Setting up Prover for', hostname);
|
||||
prover = (await new Prover({ serverDns: hostname })) as TProver;
|
||||
console.log('Setting up Prover: 1/2');
|
||||
await prover.setup(verifierProxyUrl);
|
||||
console.log('Setting up Prover: done');
|
||||
console.timeEnd('setup');
|
||||
} catch (error) {
|
||||
const msg = `Error setting up prover: ${error}`;
|
||||
console.error(msg);
|
||||
setResult(msg);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let transcript;
|
||||
try {
|
||||
console.time('request');
|
||||
console.log('Sending request to proxy');
|
||||
|
||||
const resp = await prover.sendRequest(
|
||||
`${websocketProxyUrl}?token=${hostname}`,
|
||||
{ url, method, headers, body },
|
||||
);
|
||||
console.log('Response:', resp);
|
||||
console.log('Wait for transcript');
|
||||
transcript = await prover.transcript();
|
||||
console.log('Transcript:', transcript);
|
||||
console.timeEnd('request');
|
||||
} catch (error) {
|
||||
const msg = `Error sending request: ${error}`;
|
||||
console.error(msg);
|
||||
setResult(msg);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { sent, recv } = transcript;
|
||||
const {
|
||||
info: recvInfo,
|
||||
headers: recvHeaders,
|
||||
body: recvBody,
|
||||
} = parseHttpMessage(Buffer.from(recv), 'response');
|
||||
|
||||
const body = JSON.parse(recvBody[0].toString());
|
||||
|
||||
console.time('reveal');
|
||||
const reveal: Commit = {
|
||||
sent: subtractRanges(
|
||||
{ start: 0, end: sent.length },
|
||||
mapStringToRange(
|
||||
['secret: test_secret'],
|
||||
Buffer.from(sent).toString('utf-8'),
|
||||
),
|
||||
),
|
||||
recv: [
|
||||
...mapStringToRange(
|
||||
[
|
||||
recvInfo,
|
||||
`${recvHeaders[4]}: ${recvHeaders[5]}\r\n`,
|
||||
`${recvHeaders[6]}: ${recvHeaders[7]}\r\n`,
|
||||
`${recvHeaders[8]}: ${recvHeaders[9]}\r\n`,
|
||||
`${recvHeaders[10]}: ${recvHeaders[11]}\r\n`,
|
||||
`${recvHeaders[12]}: ${recvHeaders[13]}`,
|
||||
`${recvHeaders[14]}: ${recvHeaders[15]}`,
|
||||
`${recvHeaders[16]}: ${recvHeaders[17]}`,
|
||||
`${recvHeaders[18]}: ${recvHeaders[19]}`,
|
||||
`"name":"${body.name}"`,
|
||||
`"gender":"${body.gender}"`,
|
||||
`"eye_color":"${body.eye_color}"`,
|
||||
],
|
||||
Buffer.from(recv).toString('utf-8'),
|
||||
),
|
||||
],
|
||||
};
|
||||
console.log('Start reveal:', reveal);
|
||||
await prover.reveal(reveal);
|
||||
console.timeEnd('reveal');
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
console.error('Error during data reveal:', error);
|
||||
setResult(`${error}`);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Ready');
|
||||
|
||||
console.log('Unredacted data:', {
|
||||
sent: transcript.sent,
|
||||
received: transcript.recv,
|
||||
});
|
||||
|
||||
setResult(
|
||||
"Unredacted data successfully revealed to Verifier. Check the Verifier's console output to see what exactly was shared and revealed.",
|
||||
);
|
||||
|
||||
setProcessing(false);
|
||||
}, [setResult, setProcessing]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full min-h-screen bg-gray-50 p-4">
|
||||
<h1 className="text-4xl font-bold text-slate-500 mb-2">TLSNotary</h1>
|
||||
<span className="text-lg text-gray-600 mb-4">
|
||||
Interactive Prover Demo
|
||||
</span>
|
||||
|
||||
<div className="text-center text-gray-700 mb-6">
|
||||
<p>
|
||||
Before clicking the <span className="font-semibold">Start</span>{' '}
|
||||
button, make sure the <i>interactive verifier</i> and the{' '}
|
||||
<i>web socket proxy</i> are running.
|
||||
</p>
|
||||
<p>
|
||||
Check the{' '}
|
||||
<a href="README.md" className="text-blue-600 hover:underline">
|
||||
README
|
||||
</a>{' '}
|
||||
for the details.
|
||||
</p>
|
||||
<table className="text-left table-auto w-full mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left">Demo Settings</th>
|
||||
<th className="px-4 py-2 text-left">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Server</td>
|
||||
<td className="border px-4 py-2">{serverUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Verifier</td>
|
||||
<td className="border px-4 py-2">{verifierProxyUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">WebSocket Proxy</td>
|
||||
<td className="border px-4 py-2">{websocketProxyUrl}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={!processing ? onClick : undefined}
|
||||
disabled={processing}
|
||||
className={`px-6 py-2 rounded-lg font-medium text-white
|
||||
${processing ? 'bg-slate-400 cursor-not-allowed' : 'bg-slate-600 hover:bg-slate-700'}
|
||||
`}
|
||||
>
|
||||
Start Prover
|
||||
</button>
|
||||
|
||||
<div className="mt-6 w-full max-w-3xl text-center">
|
||||
<b className="text-lg font-medium text-gray-800">Proof: </b>
|
||||
{!processing && !result ? (
|
||||
<i className="text-gray-500">Not started yet</i>
|
||||
) : !result ? (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-gray-700 mb-2">Proving data from swapi...</p>
|
||||
<Watch
|
||||
visible={true}
|
||||
height="40"
|
||||
width="40"
|
||||
radius="48"
|
||||
color="#4A5568"
|
||||
ariaLabel="watch-loading"
|
||||
wrapperStyle={{}}
|
||||
wrapperClass=""
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Open <i>Developer Tools</i> to follow progress
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-100 border border-gray-300 p-4 rounded-lg mt-4">
|
||||
<pre className="text-left text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
|
||||
const parser = new HTTPParser(
|
||||
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
|
||||
);
|
||||
const body: Buffer[] = [];
|
||||
let complete = false;
|
||||
let headers: string[] = [];
|
||||
|
||||
parser.onBody = (t) => {
|
||||
body.push(t);
|
||||
};
|
||||
|
||||
parser.onHeadersComplete = (res) => {
|
||||
headers = res.headers;
|
||||
};
|
||||
|
||||
parser.onMessageComplete = () => {
|
||||
complete = true;
|
||||
};
|
||||
|
||||
parser.execute(buffer);
|
||||
parser.finish();
|
||||
|
||||
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
|
||||
|
||||
return {
|
||||
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
|
||||
headers,
|
||||
body,
|
||||
};
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_websocket::{WebSocket, WebSocketUpgrade};
|
||||
use eyre::eyre;
|
||||
use hyper::{body::Incoming, server::conn::http1};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
};
|
||||
use tlsn_common::config::ProtocolConfigValidator;
|
||||
use tlsn_verifier::{SessionInfo, Verifier, VerifierConfig};
|
||||
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
net::TcpListener,
|
||||
};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use tower_service::Service;
|
||||
use tracing::{debug, error, info};
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
mod axum_websocket;
|
||||
|
||||
// Maximum number of bytes that can be sent from prover to server
|
||||
const MAX_SENT_DATA: usize = 1 << 12;
|
||||
// Maximum number of bytes that can be received by prover from server
|
||||
const MAX_RECV_DATA: usize = 1 << 14;
|
||||
|
||||
/// Global data that needs to be shared with the axum handlers
|
||||
#[derive(Clone, Debug)]
|
||||
struct VerifierGlobals {
|
||||
pub server_domain: String,
|
||||
}
|
||||
|
||||
pub async fn run_server(
|
||||
verifier_host: &str,
|
||||
verifier_port: u16,
|
||||
server_domain: &str,
|
||||
) -> Result<(), eyre::ErrReport> {
|
||||
let verifier_address = SocketAddr::new(
|
||||
IpAddr::V4(verifier_host.parse().map_err(|err| {
|
||||
eyre!("Failed to parse verifer host address from server config: {err}")
|
||||
})?),
|
||||
verifier_port,
|
||||
);
|
||||
let listener = TcpListener::bind(verifier_address)
|
||||
.await
|
||||
.map_err(|err| eyre!("Failed to bind server address to tcp listener: {err}"))?;
|
||||
|
||||
info!("Listening for TCP traffic at {}", verifier_address);
|
||||
|
||||
let protocol = Arc::new(http1::Builder::new());
|
||||
let router = Router::new()
|
||||
.route("/verify", get(ws_handler))
|
||||
.with_state(VerifierGlobals {
|
||||
server_domain: server_domain.to_string(),
|
||||
});
|
||||
|
||||
loop {
|
||||
let stream = match listener.accept().await {
|
||||
Ok((stream, _)) => stream,
|
||||
Err(err) => {
|
||||
error!("Failed to connect to prover: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
debug!("Received a prover's TCP connection");
|
||||
|
||||
let tower_service = router.clone();
|
||||
let protocol = protocol.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
info!("Accepted prover's TCP connection",);
|
||||
// Reference: https://github.com/tokio-rs/axum/blob/5201798d4e4d4759c208ef83e30ce85820c07baa/examples/low-level-rustls/src/main.rs#L67-L80
|
||||
let io = TokioIo::new(stream);
|
||||
let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
|
||||
tower_service.clone().call(request)
|
||||
});
|
||||
// Serve different requests using the same hyper protocol and axum router
|
||||
let _ = protocol
|
||||
.serve_connection(io, hyper_service)
|
||||
// use with_upgrades to upgrade connection to websocket for websocket clients
|
||||
// and to extract tcp connection for tcp clients
|
||||
.with_upgrades()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(verifier_globals): State<VerifierGlobals>,
|
||||
) -> impl IntoResponse {
|
||||
info!("Received websocket request");
|
||||
ws.on_upgrade(|socket| handle_socket(socket, verifier_globals))
|
||||
}
|
||||
|
||||
async fn handle_socket(socket: WebSocket, verifier_globals: VerifierGlobals) {
|
||||
debug!("Upgraded to websocket connection");
|
||||
let stream = WsStream::new(socket.into_inner());
|
||||
|
||||
match verifier(stream, &verifier_globals.server_domain).await {
|
||||
Ok((sent, received, _session_info)) => {
|
||||
info!("Successfully verified {}", &verifier_globals.server_domain);
|
||||
info!("Verified sent data:\n{}", sent,);
|
||||
println!("Verified received data:\n{}", received,);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed verification using websocket: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
socket: T,
|
||||
server_domain: &str,
|
||||
) -> Result<(String, String, SessionInfo), eyre::ErrReport> {
|
||||
debug!("Starting verification...");
|
||||
|
||||
// Setup Verifier.
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let verifier_config = VerifierConfig::builder()
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()
|
||||
.unwrap();
|
||||
let verifier = Verifier::new(verifier_config);
|
||||
|
||||
// Verify MPC-TLS and wait for (redacted) data.
|
||||
debug!("Starting MPC-TLS verification...");
|
||||
// Verify MPC-TLS and wait for (redacted) data.
|
||||
let (mut partial_transcript, session_info) = verifier.verify(socket.compat()).await.unwrap();
|
||||
partial_transcript.set_unauthed(0);
|
||||
|
||||
// Check sent data: check host.
|
||||
debug!("Starting sent data verification...");
|
||||
let sent = partial_transcript.sent_unsafe().to_vec();
|
||||
let sent_data = String::from_utf8(sent.clone()).expect("Verifier expected sent data");
|
||||
sent_data
|
||||
.find(server_domain)
|
||||
.ok_or_else(|| eyre!("Verification failed: Expected host {}", server_domain))?;
|
||||
|
||||
// Check received data: check json and version number.
|
||||
debug!("Starting received data verification...");
|
||||
let received = partial_transcript.received_unsafe().to_vec();
|
||||
let response = String::from_utf8(received.clone()).expect("Verifier expected received data");
|
||||
debug!("Received data: {:?}", response);
|
||||
response
|
||||
.find("eye_color")
|
||||
.ok_or_else(|| eyre!("Verification failed: missing eye_color in received data"))?;
|
||||
// Check Session info: server name.
|
||||
if session_info.server_name.as_str() != server_domain {
|
||||
return Err(eyre!("Verification failed: server name mismatches"));
|
||||
}
|
||||
|
||||
let sent_string = bytes_to_redacted_string(&sent)?;
|
||||
let received_string = bytes_to_redacted_string(&received)?;
|
||||
|
||||
Ok((sent_string, received_string, session_info))
|
||||
}
|
||||
|
||||
/// Render redacted bytes as `🙈`.
|
||||
fn bytes_to_redacted_string(bytes: &[u8]) -> Result<String, eyre::ErrReport> {
|
||||
Ok(String::from_utf8(bytes.to_vec())
|
||||
.map_err(|err| eyre!("Failed to parse bytes to redacted string: {err}"))?
|
||||
.replace('\0', "🙈"))
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use interactive_networked_verifier::run_server;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
const TRACING_FILTER: &str = "INFO";
|
||||
|
||||
const VERIFIER_HOST: &str = "0.0.0.0";
|
||||
const VERIFIER_PORT: u16 = 9816;
|
||||
|
||||
/// Make sure the following domain is the same in SERVER_URL on the prover side
|
||||
const SERVER_DOMAIN: &str = "swapi.dev";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), eyre::ErrReport> {
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| TRACING_FILTER.into()))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
run_server(VERIFIER_HOST, VERIFIER_PORT, SERVER_DOMAIN).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
59
demo/interactive/README.md
Normal file
59
demo/interactive/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Interactive Verifier Demo
|
||||
|
||||
This demo shows how to use TLSNotary **without a notary**: a direct proof between a prover and a verifier, where the verifier checks both the TLS session and the revealed data.
|
||||
|
||||
There are two prover implementations:
|
||||
- **Rust**
|
||||
- **TypeScript** (browser)
|
||||
The verifier is implemented in Rust.
|
||||
|
||||
---
|
||||
|
||||
## Interactive Verifier Demo with Rust Prover
|
||||
|
||||
1. **Start the verifier:**
|
||||
```bash
|
||||
cd verifier-rs
|
||||
cargo run --release
|
||||
```
|
||||
2. **Run the prover:**
|
||||
```bash
|
||||
cd prover-rs
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactive Verifier Demo with TypeScript Prover (Browser)
|
||||
|
||||
1. **Start the verifier:**
|
||||
```bash
|
||||
cd verifier-rs
|
||||
cargo run --release
|
||||
```
|
||||
2. **Set up a websocket proxy for raw.githubusercontent.com**
|
||||
Browsers cannot make raw TCP connections, so a websocket proxy is required:
|
||||
```bash
|
||||
cargo install wstcp
|
||||
wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443
|
||||
```
|
||||
3. **Run the prover in the browser:**
|
||||
1. **Build tlsn-js**
|
||||
```bash
|
||||
cd ..
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
2. **Build and start the TypeScript prover demo**
|
||||
```bash
|
||||
cd prover-ts
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
3. **Open the demo in your browser:**
|
||||
Go to [http://localhost:8080/](http://localhost:8080/) and click **Start Prover**.
|
||||
|
||||
---
|
||||
|
||||
**Tip:**
|
||||
If you encounter issues, make sure all dependencies are installed and the websocket proxy is running before starting the browser demo.
|
||||
8
demo/interactive/prover-ts/.gitignore
vendored
Normal file
8
demo/interactive/prover-ts/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
package-lock.json
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>React/Typescript Example</title>
|
||||
<title>TLSNotary React TypeScript Demo</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -4,7 +4,9 @@
|
||||
"description": "",
|
||||
"main": "webpack.js",
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --config webpack.js"
|
||||
"dev": "webpack-dev-server --config webpack.js",
|
||||
"start": "webpack serve --config webpack.js",
|
||||
"test": "npx playwright test"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
90
demo/interactive/prover-ts/playwright.config.ts
Normal file
90
demo/interactive/prover-ts/playwright.config.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:8080',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost:8080',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: 'wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
{
|
||||
command: 'cargo run --release',
|
||||
cwd: '../verifier-rs',
|
||||
reuseExistingServer: true,
|
||||
}
|
||||
]
|
||||
});
|
||||
313
demo/interactive/prover-ts/src/app.tsx
Normal file
313
demo/interactive/prover-ts/src/app.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import * as Comlink from 'comlink';
|
||||
import { Watch } from 'react-loader-spinner';
|
||||
import { Prover as TProver } from 'tlsn-js';
|
||||
import { type Method } from 'tlsn-wasm';
|
||||
import './app.scss';
|
||||
import { HTTPParser } from 'http-parser-js';
|
||||
import { Reveal, mapStringToRange, subtractRanges } from 'tlsn-js';
|
||||
|
||||
const { init, Prover }: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
|
||||
root.render(<App />);
|
||||
|
||||
// Simple console capture
|
||||
let capturedLogs: string[] = [];
|
||||
const originalLog = console.log;
|
||||
|
||||
const serverUrl = 'https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.13/crates/server-fixture/server/src/data/1kb.json';
|
||||
// const websocketProxyUrl = `wss://notary.pse.dev/proxy`;
|
||||
const websocketProxyUrl = 'ws://localhost:55688';
|
||||
const verifierProxyUrl = 'ws://localhost:9816/verify';
|
||||
|
||||
function App(): ReactElement {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [result, setResult] = useState<string | null>(null);
|
||||
const [consoleMessages, setConsoleMessages] = useState<string[]>([]);
|
||||
|
||||
// Simple console capture
|
||||
React.useEffect(() => {
|
||||
console.log = (...args) => {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const message = `[${timestamp}] ${args.join(' ')}`;
|
||||
capturedLogs.push(message);
|
||||
setConsoleMessages([...capturedLogs]);
|
||||
originalLog.apply(console, args);
|
||||
};
|
||||
|
||||
return () => {
|
||||
console.log = originalLog;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
setProcessing(true);
|
||||
capturedLogs = [];
|
||||
setConsoleMessages([]);
|
||||
console.log('Starting prover demo...');
|
||||
|
||||
const url = serverUrl;
|
||||
const method: Method = 'GET';
|
||||
const headers = {
|
||||
secret: "TLSNotary's private key",
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const body = {};
|
||||
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
let prover: TProver;
|
||||
try {
|
||||
console.time('setup');
|
||||
await init({ loggingLevel: 'Info' });
|
||||
console.log(`Setting up Prover for ${hostname}`);
|
||||
prover = (await new Prover({
|
||||
serverDns: hostname,
|
||||
maxRecvData: 2000
|
||||
})) as TProver;
|
||||
console.log('Setting up Prover: part 2/2');
|
||||
await prover.setup(verifierProxyUrl);
|
||||
console.log('Setting up Prover: done');
|
||||
console.timeEnd('setup');
|
||||
} catch (error) {
|
||||
const msg = `Error setting up prover: ${error}`;
|
||||
console.error(msg);
|
||||
setResult(msg);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let transcript;
|
||||
try {
|
||||
console.time('request');
|
||||
console.log('Sending request to proxy');
|
||||
|
||||
const resp = await prover.sendRequest(
|
||||
`${websocketProxyUrl}?token=${hostname}`,
|
||||
{ url, method, headers, body },
|
||||
);
|
||||
console.log('Response received');
|
||||
console.log('Wait for transcript');
|
||||
transcript = await prover.transcript();
|
||||
console.log('Transcript received');
|
||||
console.timeEnd('request');
|
||||
} catch (error) {
|
||||
const msg = `Error sending request: ${error}`;
|
||||
console.error(msg);
|
||||
setResult(msg);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { sent, recv } = transcript;
|
||||
const {
|
||||
info: recvInfo,
|
||||
headers: recvHeaders,
|
||||
body: recvBody,
|
||||
} = parseHttpMessage(Buffer.from(recv), 'response');
|
||||
|
||||
const body = JSON.parse(recvBody[0].toString());
|
||||
|
||||
console.log('Parsed response body data');
|
||||
|
||||
console.time('reveal');
|
||||
const reveal: Reveal = {
|
||||
sent: subtractRanges(
|
||||
{ start: 0, end: sent.length },
|
||||
mapStringToRange(
|
||||
['secret: test_secret'],
|
||||
Buffer.from(sent).toString('utf-8'),
|
||||
),
|
||||
),
|
||||
recv: [
|
||||
...mapStringToRange(
|
||||
[
|
||||
recvInfo,
|
||||
`${recvHeaders[4]}: ${recvHeaders[5]}\r\n`,
|
||||
`${recvHeaders[6]}: ${recvHeaders[7]}\r\n`,
|
||||
`${recvHeaders[8]}: ${recvHeaders[9]}\r\n`,
|
||||
`${recvHeaders[10]}: ${recvHeaders[11]}\r\n`,
|
||||
`${recvHeaders[12]}: ${recvHeaders[13]}`,
|
||||
`${recvHeaders[14]}: ${recvHeaders[15]}`,
|
||||
`${recvHeaders[16]}: ${recvHeaders[17]}`,
|
||||
`${recvHeaders[18]}: ${recvHeaders[19]}`,
|
||||
`"name": "${body.information.name}"`,
|
||||
`"street": "${body.information.address.street}"`,
|
||||
],
|
||||
Buffer.from(recv).toString('utf-8'),
|
||||
),
|
||||
],
|
||||
server_identity: true,
|
||||
};
|
||||
console.log('Start reveal process');
|
||||
await prover.reveal(reveal);
|
||||
console.log('Data revealed to verifier');
|
||||
console.timeEnd('reveal');
|
||||
} catch (error) {
|
||||
const msg = `Error during data reveal: ${error}`;
|
||||
console.error(msg);
|
||||
setResult(`${error}`);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Ready - proof completed successfully');
|
||||
console.log('Unredacted data revealed to verifier');
|
||||
|
||||
setResult(
|
||||
"Unredacted data successfully revealed to Verifier. Check the Verifier's console output to see what exactly was shared and revealed.",
|
||||
);
|
||||
|
||||
setProcessing(false);
|
||||
}, [setResult, setProcessing]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-slate-100">
|
||||
<div className="w-full p-4 bg-slate-800 text-white flex-shrink-0 shadow-md">
|
||||
<h1 className="text-xl font-bold">TLSNotary Interactive Prover Demo</h1>
|
||||
<span className="text-sm mt-1">
|
||||
Interactive Prover Demo
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 p-4 flex-grow">
|
||||
<div className="flex flex-col bg-white rounded-lg shadow-md border border-gray-200 p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Demo Controls</h2>
|
||||
|
||||
<div className="text-center text-gray-700 mb-6">
|
||||
<p>
|
||||
Before clicking the <span className="font-semibold">Start</span>{' '}
|
||||
button, make sure the <i>interactive verifier</i> and the{' '}
|
||||
<i>web socket proxy</i> are running.
|
||||
</p>
|
||||
<p>
|
||||
Check the{' '}
|
||||
<a href="README.md" className="text-blue-600 hover:underline">
|
||||
README
|
||||
</a>{' '}
|
||||
for the details.
|
||||
</p>
|
||||
<table className="text-left table-auto w-full mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left">Demo Settings</th>
|
||||
<th className="px-4 py-2 text-left">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Server</td>
|
||||
<td className="border px-4 py-2">{serverUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Verifier</td>
|
||||
<td className="border px-4 py-2">{verifierProxyUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">WebSocket Proxy</td>
|
||||
<td className="border px-4 py-2">{websocketProxyUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Prover</td>
|
||||
<td className="border px-4 py-2">This browser</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={!processing ? onClick : undefined}
|
||||
disabled={processing}
|
||||
className={`px-6 py-2 rounded-lg font-medium text-white mb-4
|
||||
${processing ? 'bg-slate-400 cursor-not-allowed' : 'bg-slate-600 hover:bg-slate-700'}
|
||||
`}
|
||||
>
|
||||
Start Prover
|
||||
</button>
|
||||
|
||||
{/* Console Log View */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-semibold text-gray-800 mb-2">Console Log</h3>
|
||||
<div className="flex flex-col text-sm bg-slate-50 border border-slate-200 w-full h-48 py-2 overflow-y-auto rounded">
|
||||
{consoleMessages.map((m, index) => (
|
||||
<span
|
||||
key={index}
|
||||
data-testid="console-log"
|
||||
className="px-3 py-1 text-slate-600 break-all"
|
||||
>
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full text-center">
|
||||
<b className="text-lg font-medium text-gray-800">Proof: </b>
|
||||
{!processing && !result ? (
|
||||
<i className="text-gray-500">Not started yet</i>
|
||||
) : !result ? (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-gray-700 mb-2">Proving data from GitHub...</p>
|
||||
<Watch
|
||||
visible={true}
|
||||
height="40"
|
||||
width="40"
|
||||
radius="48"
|
||||
color="#4A5568"
|
||||
ariaLabel="watch-loading"
|
||||
wrapperStyle={{}}
|
||||
wrapperClass=""
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-100 border border-gray-300 p-4 rounded-lg mt-4">
|
||||
<pre data-testid="proof-data" className="text-left text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{result}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
|
||||
const parser = new HTTPParser(
|
||||
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
|
||||
);
|
||||
const body: Buffer[] = [];
|
||||
let complete = false;
|
||||
let headers: string[] = [];
|
||||
|
||||
parser.onBody = (t) => {
|
||||
body.push(t);
|
||||
};
|
||||
|
||||
parser.onHeadersComplete = (res) => {
|
||||
headers = res.headers;
|
||||
};
|
||||
|
||||
parser.onMessageComplete = () => {
|
||||
complete = true;
|
||||
};
|
||||
|
||||
parser.execute(buffer);
|
||||
parser.finish();
|
||||
|
||||
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
|
||||
|
||||
return {
|
||||
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
|
||||
headers,
|
||||
body,
|
||||
};
|
||||
}
|
||||
16
demo/interactive/prover-ts/tests/demo-flow.spec.ts
Normal file
16
demo/interactive/prover-ts/tests/demo-flow.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/TLSNotary/)
|
||||
});
|
||||
|
||||
test('run demo', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('button', { name: 'Start Prover' }).click();
|
||||
|
||||
await expect(page.getByTestId('proof-data')).toContainText('Unredacted data successfully revealed to Verifier', { timeout: 60000 });
|
||||
|
||||
});
|
||||
@@ -1,38 +1,36 @@
|
||||
[package]
|
||||
name = "interactive-networked-verifier"
|
||||
name = "tlsn-demo-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.67"
|
||||
async-tungstenite = { version = "0.25", features = ["tokio-native-tls"] }
|
||||
async-tungstenite = { version = "0.25", features = ["tokio-runtime"] }
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
axum-core = "0.4"
|
||||
base64 = "0.21.0"
|
||||
eyre = "0.6.12"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3.28"
|
||||
http = { version = "1.1" }
|
||||
http-body-util = { version = "0.1" }
|
||||
hyper = { version = "1.1", features = ["client", "http1", "server"] }
|
||||
hyper-util = { version = "0.1", features = ["full"] }
|
||||
regex = "1.10.3"
|
||||
serde = { version = "1.0.147", features = ["derive"] }
|
||||
sha1 = "0.10"
|
||||
tokio = {version = "1", features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"net",
|
||||
"io-std",
|
||||
"fs",
|
||||
]}
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "io-std", "fs", "process"] }
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
tower = { version = "0.4.12", features = ["make"] }
|
||||
tower-service = { version = "0.3" }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version ="0.3.18", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
uuid = { version = "1.4.1", features = ["v4", "fast-rng"] }
|
||||
ws_stream_tungstenite = { version = "0.13", features = ["tokio_io"] }
|
||||
|
||||
tlsn-core = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.8", package = "tlsn-core" }
|
||||
tlsn-verifier = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.8", package = "tlsn-verifier" }
|
||||
tlsn-common = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.8", package = "tlsn-common" }
|
||||
tlsn = { git = "https://github.com/tlsnotary/tlsn.git", branch = "dev" }
|
||||
spansy = { git = "https://github.com/tlsnotary/tlsn-utils", package = "spansy", branch = "dev" }
|
||||
rangeset = "0.2.0"
|
||||
tower-util = "0.3.1"
|
||||
wstcp = { git = "https://github.com/sile/wstcp.git", version = "0.2.1" }
|
||||
async-std = "1.13.2"
|
||||
@@ -3,7 +3,7 @@
|
||||
An implementation of the interactive verifier server in Rust.
|
||||
|
||||
## Running the server
|
||||
1. Configure this server setting via the global variables defined in [main.rs](./src/main.rs) — please ensure that the hardcoded `SERVER_DOMAIN` and `VERIFICATION_SESSION_ID` have the same values on the prover side.
|
||||
1. Configure this server setting via the global variables defined in [main.rs](./src/main.rs) — please ensure that the hardcoded `SERVER_DOMAIN` has the same value on the prover side.
|
||||
2. Start the server by running the following in a terminal at the root of this crate.
|
||||
```bash
|
||||
cargo run --release
|
||||
39
demo/interactive/tlsn-demo-server/src/config.rs
Normal file
39
demo/interactive/tlsn-demo-server/src/config.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use http::Uri;
|
||||
/// Configuration constants for the TLSNotary demo server
|
||||
|
||||
/// Maximum number of bytes that can be sent from prover to server
|
||||
pub const MAX_SENT_DATA: usize = 2048;
|
||||
|
||||
/// Maximum number of bytes that can be received by prover from server
|
||||
pub const MAX_RECV_DATA: usize = 4096;
|
||||
|
||||
/// Secret key used in demo requests (should be redacted in proofs)
|
||||
pub const SECRET: &str = "TLSNotary's private key 🤡";
|
||||
|
||||
/// Default server configuration
|
||||
pub struct Config {
|
||||
pub ws_host: String, // Address for WebSocket server
|
||||
pub ws_port: u16, // Port for WebSocket server
|
||||
pub server_uri: Uri, // URI of the server from which data is proven with TLSNotary
|
||||
pub wstcp_proxy_port: u16, // Port for the wstcp proxy server
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ws_host: "0.0.0.0".into(),
|
||||
ws_port: 9816,
|
||||
server_uri:
|
||||
"https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.13/crates/server-fixture/server/src/data/1kb.json".parse::<Uri>().unwrap(),
|
||||
wstcp_proxy_port: 55688,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Config {
|
||||
pub fn server_domain(&self) -> String {
|
||||
self.server_uri
|
||||
.host()
|
||||
.expect("Server URL must have a valid domain")
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
158
demo/interactive/tlsn-demo-server/src/lib.rs
Normal file
158
demo/interactive/tlsn-demo-server/src/lib.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_websocket::{WebSocket, WebSocketUpgrade};
|
||||
use eyre::eyre;
|
||||
use http::Uri;
|
||||
use hyper::{body::Incoming, server::conn::http1};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_service::Service;
|
||||
use tracing::{debug, error, info};
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
mod axum_websocket;
|
||||
pub mod config;
|
||||
pub mod prover;
|
||||
pub mod verifier;
|
||||
use prover::prover;
|
||||
use verifier::verifier;
|
||||
|
||||
/// Global data that needs to be shared with the axum handlers
|
||||
#[derive(Clone, Debug)]
|
||||
struct ServerGlobals {
|
||||
pub server_uri: Uri,
|
||||
}
|
||||
|
||||
/// Enum to differentiate between prover and verifier socket handling
|
||||
#[derive(Clone, Debug)]
|
||||
enum SocketType {
|
||||
Prover,
|
||||
Verifier,
|
||||
}
|
||||
|
||||
pub async fn run_ws_server(config: &config::Config) -> Result<(), eyre::ErrReport> {
|
||||
let ws_server_address = SocketAddr::new(
|
||||
IpAddr::V4(config.ws_host.parse().map_err(|err| {
|
||||
eyre!("Failed to parse websocket host address from server config: {err}")
|
||||
})?),
|
||||
config.ws_port,
|
||||
);
|
||||
let listener = TcpListener::bind(ws_server_address)
|
||||
.await
|
||||
.map_err(|err| eyre!("Failed to bind server address to tcp listener: {err}"))?;
|
||||
|
||||
info!("Listening for TCP traffic at {}", ws_server_address);
|
||||
|
||||
let protocol = Arc::new(http1::Builder::new());
|
||||
let router = Router::new()
|
||||
.route(
|
||||
"/prove",
|
||||
get(|ws, state| ws_handler(ws, state, SocketType::Prover)),
|
||||
)
|
||||
.route(
|
||||
"/verify",
|
||||
get(|ws, state| ws_handler(ws, state, SocketType::Verifier)),
|
||||
)
|
||||
.with_state(ServerGlobals {
|
||||
server_uri: config.server_uri.clone(),
|
||||
});
|
||||
|
||||
loop {
|
||||
let stream = match listener.accept().await {
|
||||
Ok((stream, _)) => stream,
|
||||
Err(err) => {
|
||||
error!("Failed to accept TCP connection: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
debug!("Received TCP connection");
|
||||
|
||||
let tower_service = router.clone();
|
||||
let protocol = protocol.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
info!("Accepted TCP connection");
|
||||
// Reference: https://github.com/tokio-rs/axum/blob/5201798d4e4d4759c208ef83e30ce85820c07baa/examples/low-level-rustls/src/main.rs#L67-L80
|
||||
let io = TokioIo::new(stream);
|
||||
let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
|
||||
tower_service.clone().call(request)
|
||||
});
|
||||
// Serve different requests using the same hyper protocol and axum router
|
||||
if let Err(err) = protocol
|
||||
.serve_connection(io, hyper_service)
|
||||
// use with_upgrades to upgrade connection to websocket for websocket clients
|
||||
// and to extract tcp connection for tcp clients
|
||||
.with_upgrades()
|
||||
.await
|
||||
{
|
||||
error!("Connection serving failed: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(globals): State<ServerGlobals>,
|
||||
socket_type: SocketType,
|
||||
) -> impl IntoResponse {
|
||||
let operation = match socket_type {
|
||||
SocketType::Prover => "proving",
|
||||
SocketType::Verifier => "verification",
|
||||
};
|
||||
info!("Received websocket request for {}", operation);
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, globals, socket_type))
|
||||
}
|
||||
|
||||
async fn handle_socket(socket: WebSocket, globals: ServerGlobals, socket_type: SocketType) {
|
||||
let stream = WsStream::new(socket.into_inner());
|
||||
|
||||
async fn handle_operation_result<T>(
|
||||
result: Result<T, eyre::ErrReport>,
|
||||
operation: &str,
|
||||
on_success: impl FnOnce(T),
|
||||
) {
|
||||
match result {
|
||||
Ok(value) => {
|
||||
info!("============================================");
|
||||
info!("{} successful!", operation);
|
||||
info!("============================================");
|
||||
on_success(value);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{} failed: {err}", operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match socket_type {
|
||||
SocketType::Prover => {
|
||||
let result = prover(stream, &globals.server_uri).await;
|
||||
handle_operation_result(result, "Proving", |_| {}).await;
|
||||
}
|
||||
SocketType::Verifier => {
|
||||
let domain = globals
|
||||
.server_uri
|
||||
.authority()
|
||||
.ok_or_else(|| error!("Failed to extract domain from server URI"))
|
||||
.unwrap()
|
||||
.host();
|
||||
|
||||
let result = verifier(stream, domain).await;
|
||||
handle_operation_result(result, "Verification", |(sent, received)| {
|
||||
info!("Successfully verified {}", domain);
|
||||
info!("Verified sent data:\n{}", sent);
|
||||
info!("Verified received data:\n{}", received);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
demo/interactive/tlsn-demo-server/src/main.rs
Normal file
53
demo/interactive/tlsn-demo-server/src/main.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
|
||||
use tlsn_demo_server::{config::Config, run_ws_server};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
use wstcp::ProxyServer;
|
||||
|
||||
const TRACING_FILTER: &str = "INFO";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), eyre::ErrReport> {
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| TRACING_FILTER.into()))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let config: Config = Config::default();
|
||||
|
||||
// Start wstcp proxy subprocess in background
|
||||
|
||||
// Run both servers in parallel
|
||||
let (ws_result, proxy_result) =
|
||||
tokio::join!(run_ws_server(&config), run_wstcp_proxy_async(&config));
|
||||
|
||||
// Handle results - if either fails, propagate the error
|
||||
ws_result?;
|
||||
proxy_result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_wstcp_proxy_async(config: &Config) -> Result<(), eyre::ErrReport> {
|
||||
let bind_addr = SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
config.wstcp_proxy_port,
|
||||
);
|
||||
let tcp_server_addr = format!("{}:443", config.server_domain())
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| eyre::eyre!("Failed to resolve hostname"))?;
|
||||
|
||||
let listener = async_std::net::TcpListener::bind(bind_addr)
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!("Failed to bind proxy listener: {}", e))?;
|
||||
|
||||
let proxy = ProxyServer::new(listener.incoming(), tcp_server_addr)
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!("Failed to create proxy server: {}", e))?;
|
||||
|
||||
proxy
|
||||
.await
|
||||
.map_err(|e| eyre::eyre!("Proxy server error: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
149
demo/interactive/tlsn-demo-server/src/prover.rs
Normal file
149
demo/interactive/tlsn-demo-server/src/prover.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use http_body_util::Empty;
|
||||
use hyper::{body::Bytes, Request, StatusCode, Uri};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use rangeset::RangeSet;
|
||||
use spansy::{
|
||||
http::parse_response,
|
||||
json::{self},
|
||||
Spanned,
|
||||
};
|
||||
|
||||
use crate::config::{MAX_RECV_DATA, MAX_SENT_DATA, SECRET};
|
||||
use tlsn::config::ProtocolConfig;
|
||||
use tlsn::connection::ServerName;
|
||||
use tlsn::prover::{ProveConfig, ProveConfigBuilder, Prover, ProverConfig};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::{debug, info};
|
||||
|
||||
pub async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
verifier_socket: T,
|
||||
server_uri: &Uri,
|
||||
) -> Result<(), eyre::ErrReport> {
|
||||
debug!("Starting proving...");
|
||||
|
||||
assert_eq!(server_uri.scheme().unwrap().as_str(), "https");
|
||||
let server_domain = server_uri.authority().unwrap().host();
|
||||
let server_port = server_uri.port_u16().unwrap_or(443);
|
||||
|
||||
// Create prover and connect to verifier.
|
||||
let prover_config = ProverConfig::builder()
|
||||
.server_name(ServerName::Dns(server_domain.try_into().unwrap()))
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Perform the setup phase with the verifier.
|
||||
let prover = Prover::new(prover_config)
|
||||
.setup(verifier_socket.compat())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Connect to TLS Server.
|
||||
let tls_client_socket = tokio::net::TcpStream::connect((server_domain, server_port))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Pass server connection into the prover.
|
||||
let (mpc_tls_connection, prover_fut) =
|
||||
prover.connect(tls_client_socket.compat()).await.unwrap();
|
||||
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
|
||||
|
||||
// Spawn the prover task to be run concurrently in the background.
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// MPC-TLS Handshake.
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::spawn(connection);
|
||||
|
||||
// MPC-TLS: Send Request and wait for Response.
|
||||
info!("Send Request and wait for Response");
|
||||
let request = Request::builder()
|
||||
.uri(server_uri.clone())
|
||||
.header("Host", server_domain)
|
||||
.header("Connection", "close")
|
||||
.header("Secret", SECRET)
|
||||
.method("GET")
|
||||
.body(Empty::<Bytes>::new())
|
||||
.unwrap();
|
||||
let response = request_sender.send_request(request).await.unwrap();
|
||||
|
||||
debug!("TLS response: {:?}", response);
|
||||
assert!(response.status() == StatusCode::OK);
|
||||
|
||||
// Create proof for the Verifier.
|
||||
let mut prover = prover_task.await.unwrap().unwrap();
|
||||
|
||||
info!(
|
||||
"server signature: {:?}",
|
||||
prover.tls_transcript().server_signature().unwrap().scheme,
|
||||
);
|
||||
let mut builder: ProveConfigBuilder<'_> = ProveConfig::builder(prover.transcript());
|
||||
|
||||
// Reveal the DNS name.
|
||||
builder.server_identity();
|
||||
|
||||
let sent_rangeset = redact_and_reveal_sent_data(prover.transcript().sent());
|
||||
let _ = builder.reveal_sent(&sent_rangeset);
|
||||
|
||||
let recv_rangeset = redact_and_reveal_received_data(prover.transcript().received());
|
||||
let _ = builder.reveal_recv(&recv_rangeset);
|
||||
|
||||
let config = builder.build().unwrap();
|
||||
|
||||
prover.prove(&config).await.unwrap();
|
||||
prover.close().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redacts and reveals received data to the verifier.
|
||||
fn redact_and_reveal_received_data(recv_transcript: &[u8]) -> RangeSet<usize> {
|
||||
// Get the some information from the received data.
|
||||
let received_string = String::from_utf8(recv_transcript.to_vec()).unwrap();
|
||||
debug!("Received data: {}", received_string);
|
||||
let resp = parse_response(recv_transcript).unwrap();
|
||||
let body = resp.body.unwrap();
|
||||
let mut json = json::parse_slice(body.as_bytes()).unwrap();
|
||||
json.offset(body.content.span().indices().min().unwrap());
|
||||
|
||||
let name = json.get("information.name").expect("name field not found");
|
||||
|
||||
let street = json
|
||||
.get("information.address.street")
|
||||
.expect("street field not found");
|
||||
|
||||
let name_start = name.span().indices().min().unwrap() - 9; // 9 is the length of "name: "
|
||||
let name_end = name.span().indices().max().unwrap() + 1; // include `"`
|
||||
let street_start = street.span().indices().min().unwrap() - 11; // 11 is the length of "street: "
|
||||
let street_end = street.span().indices().max().unwrap() + 1; // include `"`
|
||||
|
||||
[name_start..name_end + 1, street_start..street_end + 1].into()
|
||||
}
|
||||
|
||||
/// Redacts and reveals sent data to the verifier.
|
||||
fn redact_and_reveal_sent_data(sent_transcript: &[u8]) -> RangeSet<usize> {
|
||||
let sent_transcript_len = sent_transcript.len();
|
||||
|
||||
let sent_string: String = String::from_utf8(sent_transcript.to_vec()).unwrap();
|
||||
let secret_start = sent_string.find(SECRET).unwrap();
|
||||
|
||||
debug!("Send data: {}", sent_string);
|
||||
|
||||
// Reveal everything except for the SECRET.
|
||||
[
|
||||
0..secret_start,
|
||||
secret_start + SECRET.len()..sent_transcript_len,
|
||||
]
|
||||
.into()
|
||||
}
|
||||
90
demo/interactive/tlsn-demo-server/src/verifier.rs
Normal file
90
demo/interactive/tlsn-demo-server/src/verifier.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crate::config::{MAX_RECV_DATA, MAX_SENT_DATA};
|
||||
use eyre::eyre;
|
||||
use tlsn::{
|
||||
config::ProtocolConfigValidator,
|
||||
connection::ServerName,
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Core verifier logic that validates the TLS proof
|
||||
pub async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
socket: T,
|
||||
server_domain: &str,
|
||||
) -> Result<(String, String), eyre::ErrReport> {
|
||||
debug!("Starting verification...");
|
||||
|
||||
// Setup Verifier.
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let verifier_config = VerifierConfig::builder()
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()
|
||||
.unwrap();
|
||||
let verifier = Verifier::new(verifier_config);
|
||||
|
||||
// Receive authenticated data.
|
||||
debug!("Starting MPC-TLS verification...");
|
||||
|
||||
let VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
..
|
||||
} = verifier
|
||||
.verify(socket.compat(), &VerifyConfig::default())
|
||||
.await
|
||||
.map_err(|e| eyre!("Verification failed: {}", e))?;
|
||||
|
||||
let server_name =
|
||||
server_name.ok_or_else(|| eyre!("prover should have revealed server name"))?;
|
||||
let transcript =
|
||||
transcript.ok_or_else(|| eyre!("prover should have revealed transcript data"))?;
|
||||
|
||||
// Check sent data: check host.
|
||||
debug!("Starting sent data verification...");
|
||||
let sent = transcript.sent_unsafe().to_vec();
|
||||
let sent_data = String::from_utf8(sent.clone()).expect("Verifier expected sent data");
|
||||
sent_data
|
||||
.find(server_domain)
|
||||
.ok_or_else(|| eyre!("Verification failed: Expected host {}", server_domain))?;
|
||||
|
||||
// Check received data: check json and version number.
|
||||
debug!("Starting received data verification...");
|
||||
let received = transcript.received_unsafe().to_vec();
|
||||
let response = String::from_utf8(received.clone()).expect("Verifier expected received data");
|
||||
|
||||
debug!("Received data: {:?}", response);
|
||||
response
|
||||
.find("123 Elm Street")
|
||||
.ok_or_else(|| eyre!("Verification failed: missing data in received data"))?;
|
||||
|
||||
// Check Session info: server name.
|
||||
let ServerName::Dns(dns_name) = server_name;
|
||||
if dns_name.as_str() != server_domain {
|
||||
return Err(eyre!("Verification failed: server name mismatches"));
|
||||
}
|
||||
|
||||
let sent_string = bytes_to_redacted_string(&sent)?;
|
||||
let received_string = bytes_to_redacted_string(&received)?;
|
||||
|
||||
info!("============================================");
|
||||
info!("Verification successful!");
|
||||
info!("============================================");
|
||||
info!("Sent data: {:?}", sent_string);
|
||||
info!("Received data: {:?}", received_string);
|
||||
|
||||
Ok((sent_string, received_string))
|
||||
}
|
||||
|
||||
/// Render redacted bytes as `🙈`.
|
||||
fn bytes_to_redacted_string(bytes: &[u8]) -> Result<String, eyre::ErrReport> {
|
||||
Ok(String::from_utf8(bytes.to_vec())
|
||||
.map_err(|err| eyre!("Failed to parse bytes to redacted string: {err}"))?
|
||||
.replace('\0', "🙈"))
|
||||
}
|
||||
138
demo/interactive/tlsn-demo-server/tests/integration_test.rs
Normal file
138
demo/interactive/tlsn-demo-server/tests/integration_test.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use async_tungstenite::{tokio::connect_async_with_config, tungstenite::protocol::WebSocketConfig};
|
||||
use eyre::eyre;
|
||||
use std::time::Duration;
|
||||
use tlsn_demo_server::{config::Config, prover::prover, run_ws_server, verifier::verifier};
|
||||
use tokio::time::timeout;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
use uuid;
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
const TRACING_FILTER: &str = "INFO";
|
||||
const SERVER_START_DELAY: Duration = Duration::from_millis(500);
|
||||
const TEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
fn init_tracing() {
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| TRACING_FILTER.into()))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.try_init();
|
||||
}
|
||||
|
||||
async fn start_test_server() -> tokio::task::JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let config = Config::default();
|
||||
run_ws_server(&config)
|
||||
.await
|
||||
.expect("Server should start successfully")
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a WebSocket connection request with standard headers
|
||||
/// This eliminates duplication of WebSocket request creation across modules
|
||||
pub fn create_websocket_request(host: &str, port: u16, path: &str) -> http::Request<()> {
|
||||
http::Request::builder()
|
||||
.uri(format!("ws://{host}:{port}{path}"))
|
||||
.header("Host", host)
|
||||
.header("Sec-WebSocket-Key", uuid::Uuid::new_v4().to_string())
|
||||
.header("Sec-WebSocket-Version", "13")
|
||||
.header("Connection", "Upgrade")
|
||||
.header("Upgrade", "Websocket")
|
||||
.body(())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_prover_verifier_integration() {
|
||||
init_tracing();
|
||||
|
||||
let server_task = start_test_server().await;
|
||||
tokio::time::sleep(SERVER_START_DELAY).await;
|
||||
|
||||
let config = Config::default();
|
||||
let result = timeout(TEST_TIMEOUT, async {
|
||||
info!("Connecting to server as verifier...");
|
||||
let request = create_websocket_request(&config.ws_host, config.ws_port, "/prove");
|
||||
let (ws_stream, _) = connect_async_with_config(request, Some(WebSocketConfig::default()))
|
||||
.await
|
||||
.map_err(|e| eyre!("Failed to connect to server: {}", e))?;
|
||||
let server_ws_socket = WsStream::new(ws_stream);
|
||||
info!("WebSocket connection established with server!");
|
||||
verifier(server_ws_socket, &config.server_domain()).await?;
|
||||
info!("Verification completed successfully!");
|
||||
Ok::<(), eyre::ErrReport>(())
|
||||
})
|
||||
.await;
|
||||
|
||||
server_task.abort();
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => {
|
||||
println!("✅ Integration test passed: Prover-Verifier communication successful")
|
||||
}
|
||||
Ok(Err(e)) => panic!("❌ Test failed: {}", e),
|
||||
Err(_) => panic!("❌ Test timed out after {:?}", TEST_TIMEOUT),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_verifier_prover_integration() {
|
||||
init_tracing();
|
||||
|
||||
let server_task = start_test_server().await;
|
||||
tokio::time::sleep(SERVER_START_DELAY).await;
|
||||
|
||||
let config = Config::default();
|
||||
let result = timeout(TEST_TIMEOUT, async {
|
||||
info!("Connecting to server as prover...");
|
||||
let request = create_websocket_request(&config.ws_host, config.ws_port, "/verify");
|
||||
let (ws_stream, _) = connect_async_with_config(request, Some(WebSocketConfig::default()))
|
||||
.await
|
||||
.map_err(|e| eyre!("Failed to connect to server: {}", e))?;
|
||||
let server_ws_socket = WsStream::new(ws_stream);
|
||||
info!("WebSocket connection established with server!");
|
||||
prover(server_ws_socket, &config.server_uri).await?;
|
||||
info!("Proving completed successfully!");
|
||||
Ok::<(), eyre::ErrReport>(())
|
||||
})
|
||||
.await;
|
||||
|
||||
server_task.abort();
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => {
|
||||
println!("✅ Integration test passed: Verifier-Prover communication successful")
|
||||
}
|
||||
Ok(Err(e)) => panic!("❌ Test failed: {}", e),
|
||||
Err(_) => panic!("❌ Test timed out after {:?}", TEST_TIMEOUT),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_verifier_connection_failure() {
|
||||
init_tracing();
|
||||
|
||||
let config = Config {
|
||||
ws_port: 54321, // Non-existent port
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let result = timeout(Duration::from_secs(5), async {
|
||||
info!("Connecting to server as verifier...");
|
||||
let request = create_websocket_request(&config.ws_host, config.ws_port, "/prove");
|
||||
let (ws_stream, _) = connect_async_with_config(request, Some(WebSocketConfig::default()))
|
||||
.await
|
||||
.map_err(|e| eyre!("Failed to connect to server: {}", e))?;
|
||||
let server_ws_socket = WsStream::new(ws_stream);
|
||||
info!("WebSocket connection established with server!");
|
||||
verifier(server_ws_socket, &config.server_domain()).await?;
|
||||
info!("Verification completed successfully!");
|
||||
Ok::<(), eyre::ErrReport>(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => panic!("Should not succeed when connecting to non-existent server"),
|
||||
Ok(Err(_)) => println!("✅ Correctly failed to connect to non-existent server"),
|
||||
Err(_) => println!("✅ Correctly timed out when connecting to non-existent server"),
|
||||
}
|
||||
}
|
||||
8
demo/interactive/verifier-ts/.gitignore
vendored
Normal file
8
demo/interactive/verifier-ts/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
package-lock.json
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
@@ -4,13 +4,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>React/Typescrip Example</title>
|
||||
<title>TLSNotary React TypeScript Demo</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,44 +1,41 @@
|
||||
{
|
||||
"name": "react-ts-webpack",
|
||||
"name": "verifier-ts",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "webpack.js",
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --config webpack.js",
|
||||
"build": "webpack --config webpack.js"
|
||||
"start": "webpack serve --config webpack.js",
|
||||
"test": "npx playwright test"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"comlink": "^4.4.1",
|
||||
"@fortawesome/fontawesome-free": "^6.7.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"http-parser-js": "^0.5.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
"sass": "^1.83.1",
|
||||
"sass-loader": "^16.0.4",
|
||||
"style-loader": "^4.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tlsn-js": "../../"
|
||||
"tlsn-js": "../../.."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"postcss-preset-env": "^10.1.3",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-preset-env": "^10.1.1",
|
||||
"sass": "^1.82.0",
|
||||
"sass-loader": "^16.0.4",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
90
demo/interactive/verifier-ts/playwright.config.ts
Normal file
90
demo/interactive/verifier-ts/playwright.config.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:8080',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost:8080',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: 'wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
{
|
||||
command: 'cargo run --release',
|
||||
cwd: '../verifier-rs',
|
||||
reuseExistingServer: true,
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
const tailwindcss = require("tailwindcss");
|
||||
module.exports = {
|
||||
plugins: ["postcss-preset-env", tailwindcss],
|
||||
};
|
||||
};
|
||||
255
demo/interactive/verifier-ts/src/app.tsx
Normal file
255
demo/interactive/verifier-ts/src/app.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import * as Comlink from 'comlink';
|
||||
import { Watch } from 'react-loader-spinner';
|
||||
import { Verifier as TVerifier } from 'tlsn-wasm';
|
||||
import './app.scss';
|
||||
import { HTTPParser } from 'http-parser-js';
|
||||
|
||||
const { init, Verifier }: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
|
||||
root.render(<App />);
|
||||
|
||||
// Simple console capture
|
||||
let capturedLogs: string[] = [];
|
||||
const originalLog = console.log;
|
||||
|
||||
const serverUrl = 'https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.13/crates/server-fixture/server/src/data/1kb.json';
|
||||
// const websocketProxyUrl = `wss://notary.pse.dev/proxy`;
|
||||
const proverProxyUrl = 'ws://localhost:9816/prove';
|
||||
|
||||
function App(): ReactElement {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [result, setResult] = useState<string | null>(null);
|
||||
const [consoleMessages, setConsoleMessages] = useState<string[]>([]);
|
||||
|
||||
// Simple console capture
|
||||
React.useEffect(() => {
|
||||
console.log = (...args) => {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const message = `[${timestamp}] ${args.join(' ')}`;
|
||||
capturedLogs.push(message);
|
||||
setConsoleMessages([...capturedLogs]);
|
||||
originalLog.apply(console, args);
|
||||
};
|
||||
|
||||
return () => {
|
||||
console.log = originalLog;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Initialize TLSNotary
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
await init({ loggingLevel: 'Info' });
|
||||
setReady(true);
|
||||
console.log('TLSNotary initialized and ready');
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
setProcessing(true);
|
||||
capturedLogs = [];
|
||||
setConsoleMessages([]);
|
||||
console.log('Starting verifier demo...');
|
||||
|
||||
let verifier: TVerifier;
|
||||
try {
|
||||
console.log('Setting up Verifier');
|
||||
verifier = await new Verifier({
|
||||
max_sent_data: 2048,
|
||||
max_recv_data: 4096
|
||||
});
|
||||
console.log('Verifier class instantiated');
|
||||
await verifier.connect(proverProxyUrl);
|
||||
console.log('Connecting verifier to p2p proxy: done');
|
||||
} catch (e: any) {
|
||||
console.error('Error setting up verifier: ' + e.message);
|
||||
console.error('Error connecting verifier to p2p proxy: ' + e.message);
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
console.log('Start verifier');
|
||||
// This needs to be called before we send the request
|
||||
// This starts the verifier and makes it wait for the prover to send the request
|
||||
const verified = verifier.verify();
|
||||
const result = await verified;
|
||||
console.log('Verification completed');
|
||||
|
||||
const sent_b = result.transcript?.sent || [];
|
||||
const recv_b = result.transcript?.recv || [];
|
||||
|
||||
const server_name = result.server_name
|
||||
|
||||
console.log(`Verified server name: ${server_name}`);
|
||||
|
||||
let recv = bytesToUtf8(recv_b);
|
||||
let sent = bytesToUtf8(sent_b);
|
||||
|
||||
console.log('Verified data received');
|
||||
console.log(`Transcript sent: ${sent.substring(0, 100)}${sent.length > 100 ? '...' : ''}`);
|
||||
console.log(`Transcript received: ${recv.substring(0, 100)}${recv.length > 100 ? '...' : ''}`);
|
||||
|
||||
console.log('Ready - verification completed successfully');
|
||||
|
||||
setResult(`Sent to ${server_name}:\n` +
|
||||
sent +
|
||||
"\n" +
|
||||
`Received from ${server_name}:\n` +
|
||||
recv,
|
||||
);
|
||||
|
||||
setProcessing(false);
|
||||
}, [setResult, setProcessing]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-slate-100">
|
||||
<div className="w-full p-4 bg-slate-800 text-white flex-shrink-0 shadow-md">
|
||||
<h1 className="text-xl font-bold">TLSNotary Interactive Verifier Demo</h1>
|
||||
<span className="text-sm mt-1">
|
||||
Interactive Verifier Demo
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 p-4 flex-grow">
|
||||
<div className="flex flex-col bg-white rounded-lg shadow-md border border-gray-200 p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Demo Controls</h2>
|
||||
|
||||
<div className="text-center text-gray-700 mb-6">
|
||||
<p>
|
||||
Before clicking the <span className="font-semibold">Verify</span>{' '}
|
||||
button, make sure the <i>interactive Prover Server</i> is running.
|
||||
</p>
|
||||
<p>
|
||||
Check the{' '}
|
||||
<a href="README.md" className="text-blue-600 hover:underline">
|
||||
README
|
||||
</a>{' '}
|
||||
for the details.
|
||||
</p>
|
||||
<table className="text-left table-auto w-full mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left">Demo Settings</th>
|
||||
<th className="px-4 py-2 text-left">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Server</td>
|
||||
<td className="border px-4 py-2">{serverUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Prover</td>
|
||||
<td className="border px-4 py-2">{proverProxyUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Verifier</td>
|
||||
<td className="border px-4 py-2">This browser</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={!processing && ready ? onClick : undefined}
|
||||
disabled={processing || !ready}
|
||||
className={`px-6 py-2 rounded-lg font-medium text-white mb-4
|
||||
${processing || !ready ? 'bg-slate-400 cursor-not-allowed' : 'bg-slate-600 hover:bg-slate-700'}
|
||||
`}
|
||||
>
|
||||
{ready ? 'Verify Prover Server' : 'Initializing...'}
|
||||
</button>
|
||||
|
||||
<div className="w-full text-center">
|
||||
<b className="text-lg font-medium text-gray-800">Verified data: </b>
|
||||
{!processing && !result ? (
|
||||
<i className="text-gray-500">Not started yet</i>
|
||||
) : !result ? (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-gray-700 mb-2">Verifying data from Prover...</p>
|
||||
<Watch
|
||||
visible={true}
|
||||
height="40"
|
||||
width="40"
|
||||
radius="48"
|
||||
color="#4A5568"
|
||||
ariaLabel="watch-loading"
|
||||
wrapperStyle={{}}
|
||||
wrapperClass=""
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-100 border border-gray-300 p-4 rounded-lg mt-4">
|
||||
<pre data-testid="proof-data" className="text-left text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{result}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Console Log View */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-semibold text-gray-800 mb-2">Console Log</h3>
|
||||
<div className="flex flex-col text-sm bg-slate-50 border border-slate-200 w-full h-48 py-2 overflow-y-auto rounded">
|
||||
{consoleMessages.map((m, index) => (
|
||||
<span
|
||||
key={index}
|
||||
data-testid="console-log"
|
||||
className="px-3 py-1 text-slate-600 break-all"
|
||||
>
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
|
||||
const parser = new HTTPParser(
|
||||
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
|
||||
);
|
||||
const body: Buffer[] = [];
|
||||
let complete = false;
|
||||
let headers: string[] = [];
|
||||
|
||||
parser.onBody = (t) => {
|
||||
body.push(t);
|
||||
};
|
||||
|
||||
parser.onHeadersComplete = (res) => {
|
||||
headers = res.headers;
|
||||
};
|
||||
|
||||
parser.onMessageComplete = () => {
|
||||
complete = true;
|
||||
};
|
||||
|
||||
parser.execute(buffer);
|
||||
parser.finish();
|
||||
|
||||
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
|
||||
|
||||
return {
|
||||
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
|
||||
headers,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
function bytesToUtf8(array: number[]): string {
|
||||
return Buffer.from(array).toString("utf8").replaceAll('\u0000', '🙈');
|
||||
}
|
||||
32
demo/interactive/verifier-ts/src/worker.ts
Normal file
32
demo/interactive/verifier-ts/src/worker.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import initWasm, { LoggingLevel, initialize, Verifier } from 'tlsn-wasm';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Verifier,
|
||||
});
|
||||
|
||||
export default async function init(config?: {
|
||||
loggingLevel?: LoggingLevel;
|
||||
hardwareConcurrency?: number;
|
||||
}) {
|
||||
const {
|
||||
loggingLevel = 'Info',
|
||||
hardwareConcurrency = navigator.hardwareConcurrency,
|
||||
} = config || {};
|
||||
|
||||
|
||||
const res = await initWasm();
|
||||
|
||||
|
||||
await initialize(
|
||||
{
|
||||
level: loggingLevel,
|
||||
crate_filters: undefined,
|
||||
span_events: undefined,
|
||||
},
|
||||
hardwareConcurrency,
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
16
demo/interactive/verifier-ts/tests/demo-flow.spec.ts
Normal file
16
demo/interactive/verifier-ts/tests/demo-flow.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/TLSNotary/)
|
||||
});
|
||||
|
||||
test('run demo', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('button', { name: 'Start Prover' }).click();
|
||||
|
||||
await expect(page.getByTestId('proof-data')).toContainText('Unredacted data successfully revealed to Verifier', { timeout: 60000 });
|
||||
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -16,6 +20,7 @@
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src/app.tsx"
|
||||
"src/app.tsx",
|
||||
"src/worker.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,9 @@ var options = {
|
||||
},
|
||||
],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{ from: '../README.md', to: 'README.md' }],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(__dirname, 'index.ejs'),
|
||||
filename: 'index.html',
|
||||
1
demo/react-ts-webpack/.gitignore
vendored
1
demo/react-ts-webpack/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
package-lock.json
|
||||
3428
demo/react-ts-webpack/pnpm-lock.yaml
generated
3428
demo/react-ts-webpack/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,341 +0,0 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import * as Comlink from 'comlink';
|
||||
import { Watch } from 'react-loader-spinner';
|
||||
import {
|
||||
Prover as TProver,
|
||||
Presentation as TPresentation,
|
||||
Commit,
|
||||
NotaryServer,
|
||||
Transcript,
|
||||
mapStringToRange,
|
||||
subtractRanges,
|
||||
} from 'tlsn-js';
|
||||
import { PresentationJSON } from 'tlsn-js/build/types';
|
||||
import './app.scss';
|
||||
import { HTTPParser } from 'http-parser-js';
|
||||
const { init, Prover, Presentation }: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
|
||||
root.render(<App />);
|
||||
|
||||
const local = true; // Toggle between local and remote notary
|
||||
const notaryUrl = local
|
||||
? 'http://localhost:7047'
|
||||
: 'https://notary.pse.dev/v0.1.0-alpha.8';
|
||||
const websocketProxyUrl = local
|
||||
? 'ws://localhost:55688'
|
||||
: 'wss://notary.pse.dev/proxy?token=swapi.dev';
|
||||
const loggingLevel = 'Info'; // https://github.com/tlsnotary/tlsn/blob/main/crates/wasm/src/log.rs#L8
|
||||
|
||||
const serverUrl = 'https://swapi.dev/api/people/1';
|
||||
const serverDns = 'swapi.dev';
|
||||
|
||||
function App(): ReactElement {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [result, setResult] = useState<any | null>(null);
|
||||
const [presentationJSON, setPresentationJSON] =
|
||||
useState<null | PresentationJSON>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await init({ loggingLevel: loggingLevel });
|
||||
setInitialized(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
setProcessing(true);
|
||||
const notary = NotaryServer.from(notaryUrl);
|
||||
console.time('submit');
|
||||
const prover = (await new Prover({
|
||||
serverDns: serverDns,
|
||||
maxRecvData: 2048,
|
||||
})) as TProver;
|
||||
|
||||
await prover.setup(await notary.sessionUrl());
|
||||
|
||||
const resp = await prover.sendRequest(websocketProxyUrl, {
|
||||
url: serverUrl,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
secret: 'test_secret',
|
||||
},
|
||||
body: {
|
||||
hello: 'world',
|
||||
one: 1,
|
||||
},
|
||||
});
|
||||
|
||||
console.timeEnd('submit');
|
||||
console.log(resp);
|
||||
|
||||
console.time('transcript');
|
||||
const transcript = await prover.transcript();
|
||||
const { sent, recv } = transcript;
|
||||
console.log(new Transcript({ sent, recv }));
|
||||
console.timeEnd('transcript');
|
||||
console.time('commit');
|
||||
|
||||
const {
|
||||
info: recvInfo,
|
||||
headers: recvHeaders,
|
||||
body: recvBody,
|
||||
} = parseHttpMessage(Buffer.from(recv), 'response');
|
||||
|
||||
const body = JSON.parse(recvBody[0].toString());
|
||||
|
||||
const commit: Commit = {
|
||||
sent: subtractRanges(
|
||||
{ start: 0, end: sent.length },
|
||||
mapStringToRange(
|
||||
['secret: test_secret'],
|
||||
Buffer.from(sent).toString('utf-8'),
|
||||
),
|
||||
),
|
||||
recv: [
|
||||
...mapStringToRange(
|
||||
[
|
||||
recvInfo,
|
||||
`${recvHeaders[4]}: ${recvHeaders[5]}\r\n`,
|
||||
`${recvHeaders[6]}: ${recvHeaders[7]}\r\n`,
|
||||
`${recvHeaders[8]}: ${recvHeaders[9]}\r\n`,
|
||||
`${recvHeaders[10]}: ${recvHeaders[11]}\r\n`,
|
||||
`${recvHeaders[12]}: ${recvHeaders[13]}`,
|
||||
`${recvHeaders[14]}: ${recvHeaders[15]}`,
|
||||
`${recvHeaders[16]}: ${recvHeaders[17]}`,
|
||||
`${recvHeaders[18]}: ${recvHeaders[19]}`,
|
||||
`"name":"${body.name}"`,
|
||||
`"gender":"${body.gender}"`,
|
||||
],
|
||||
Buffer.from(recv).toString('utf-8'),
|
||||
),
|
||||
],
|
||||
};
|
||||
const notarizationOutputs = await prover.notarize(commit);
|
||||
console.timeEnd('commit');
|
||||
console.time('proof');
|
||||
|
||||
const presentation = (await new Presentation({
|
||||
attestationHex: notarizationOutputs.attestation,
|
||||
secretsHex: notarizationOutputs.secrets,
|
||||
notaryUrl: notarizationOutputs.notaryUrl,
|
||||
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
|
||||
reveal: commit,
|
||||
})) as TPresentation;
|
||||
|
||||
console.log(await presentation.serialize());
|
||||
setPresentationJSON(await presentation.json());
|
||||
console.timeEnd('proof');
|
||||
}, [setPresentationJSON, setProcessing]);
|
||||
|
||||
const onAltClick = useCallback(async () => {
|
||||
setProcessing(true);
|
||||
const proof = await (Prover.notarize as typeof TProver.notarize)({
|
||||
notaryUrl: notaryUrl,
|
||||
websocketProxyUrl: websocketProxyUrl,
|
||||
url: serverUrl,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
body: {
|
||||
hello: 'world',
|
||||
one: 1,
|
||||
},
|
||||
commit: {
|
||||
sent: [{ start: 0, end: 50 }],
|
||||
recv: [{ start: 0, end: 50 }],
|
||||
},
|
||||
});
|
||||
|
||||
setPresentationJSON(proof);
|
||||
}, [setPresentationJSON, setProcessing]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (presentationJSON) {
|
||||
const proof = (await new Presentation(
|
||||
presentationJSON.data,
|
||||
)) as TPresentation;
|
||||
const notary = NotaryServer.from(notaryUrl);
|
||||
const notaryKey = await notary.publicKey('hex');
|
||||
const verifierOutput = await proof.verify();
|
||||
const transcript = new Transcript({
|
||||
sent: verifierOutput.transcript.sent,
|
||||
recv: verifierOutput.transcript.recv,
|
||||
});
|
||||
const vk = await proof.verifyingKey();
|
||||
setResult({
|
||||
time: verifierOutput.connection_info.time,
|
||||
verifyingKey: Buffer.from(vk.data).toString('hex'),
|
||||
notaryKey: notaryKey,
|
||||
serverName: verifierOutput.server_name,
|
||||
sent: transcript.sent(),
|
||||
recv: transcript.recv(),
|
||||
});
|
||||
setProcessing(false);
|
||||
}
|
||||
})();
|
||||
}, [presentationJSON, setResult]);
|
||||
|
||||
return (
|
||||
<div className="bg-slate-100 min-h-screen p-6 text-slate-800 flex flex-col items-center">
|
||||
<h1 className="text-2xl font-bold mb-6 text-slate-700">
|
||||
TLSNotary React TypeScript Demo{' '}
|
||||
</h1>
|
||||
<div className="mb-4 text-base font-light max-w-2xl">
|
||||
<p>
|
||||
This demo showcases how to use TLSNotary in a React/TypeScript app
|
||||
with the tlsn-js library. We will fetch JSON data from the Star Wars
|
||||
API, notarize the TLS request using TLSNotary, and verify the proof.
|
||||
The demo runs entirely in the browser.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://docs.tlsnotary.org/quick_start/tlsn-js.html"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
More info
|
||||
</a>
|
||||
</p>
|
||||
<table className="table-auto w-full mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left">Demo Settings</th>
|
||||
<th className="px-4 py-2 text-left">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Server</td>
|
||||
<td className="border px-4 py-2">{serverUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">Notary Server</td>
|
||||
<td className="border px-4 py-2">{notaryUrl}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-4 py-2">WebSocket Proxy</td>
|
||||
<td className="border px-4 py-2">{websocketProxyUrl}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-base font-light">
|
||||
There are two versions of the demo: one with a normal config and one
|
||||
with a helper method.
|
||||
</p>
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
onClick={!processing ? onClick : undefined}
|
||||
disabled={processing || !initialized}
|
||||
className={`px-4 py-2 rounded-md text-white shadow-md font-semibold
|
||||
${processing || !initialized ? 'bg-slate-400 cursor-not-allowed' : 'bg-slate-600 hover:bg-slate-700'}`}
|
||||
>
|
||||
Start Demo (Normal config)
|
||||
</button>
|
||||
<button
|
||||
onClick={!processing ? onAltClick : undefined}
|
||||
disabled={processing || !initialized}
|
||||
className={`px-4 py-2 rounded-md text-white shadow-md font-semibold
|
||||
${processing || !initialized ? 'bg-slate-400 cursor-not-allowed' : 'bg-slate-600 hover:bg-slate-700'}`}
|
||||
>
|
||||
Start Demo 2 (With helper method)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{processing && (
|
||||
<div className="mt-6 flex justify-center items-center">
|
||||
<Watch
|
||||
visible={true}
|
||||
height="40"
|
||||
width="40"
|
||||
radius="48"
|
||||
color="#1E293B"
|
||||
ariaLabel="watch-loading"
|
||||
wrapperStyle={{}}
|
||||
wrapperClass=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col sm:flex-row gap-6 w-full max-w-4xl">
|
||||
<div className="flex-1 bg-slate-50 border border-slate-200 rounded p-4">
|
||||
<b className="text-slate-600">Proof: </b>
|
||||
{!processing && !presentationJSON ? (
|
||||
<i className="text-slate-500">not started</i>
|
||||
) : !presentationJSON ? (
|
||||
<div className="flex flex-col items-start space-y-2">
|
||||
<span>Proving data from {serverDns}...</span>
|
||||
<span className="text-slate-500">
|
||||
Open <i>Developer tools</i> to follow progress
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<details className="bg-slate-50 border border-slate-200 rounded p-2">
|
||||
<summary className="cursor-pointer text-slate-600">
|
||||
View Proof
|
||||
</summary>
|
||||
<pre className="mt-2 p-2 bg-slate-100 rounded text-sm text-slate-800">
|
||||
{JSON.stringify(presentationJSON, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 bg-slate-50 border border-slate-200 rounded p-4">
|
||||
<b className="text-slate-600">Verification: </b>
|
||||
{!presentationJSON ? (
|
||||
<i className="text-slate-500">not started</i>
|
||||
) : !result ? (
|
||||
<i className="text-slate-500">verifying</i>
|
||||
) : (
|
||||
<pre className="mt-2 p-2 bg-slate-100 rounded text-sm text-slate-800">
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
|
||||
const parser = new HTTPParser(
|
||||
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
|
||||
);
|
||||
const body: Buffer[] = [];
|
||||
let complete = false;
|
||||
let headers: string[] = [];
|
||||
|
||||
parser.onBody = (t) => {
|
||||
body.push(t);
|
||||
};
|
||||
|
||||
parser.onHeadersComplete = (res) => {
|
||||
headers = res.headers;
|
||||
};
|
||||
|
||||
parser.onMessageComplete = () => {
|
||||
complete = true;
|
||||
};
|
||||
|
||||
parser.execute(buffer);
|
||||
parser.finish();
|
||||
|
||||
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
|
||||
|
||||
return {
|
||||
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
|
||||
headers,
|
||||
body,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import init, { Prover, Attestation, Presentation } from 'tlsn-js';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Prover,
|
||||
Presentation,
|
||||
Attestation,
|
||||
});
|
||||
7
demo/web-to-web-p2p/.gitignore
vendored
7
demo/web-to-web-p2p/.gitignore
vendored
@@ -1 +1,8 @@
|
||||
package-lock.json
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Web-to-Web P2P Demo
|
||||
|
||||
This project demonstrates a peer-to-peer (P2P) communication between two web clients using TLSNotary.
|
||||
The web prover will get data from <https://swapi.dev> and prove it to the web verifier.
|
||||
The web prover will get data from <https://raw.githubusercontent.com> and prove it to the web verifier.
|
||||
|
||||
In this demo, the two web clients run in the same browser page (`./src/app.tsx`) and communicate via a simple websocket server (`./server/index.js`)
|
||||
|
||||
@@ -12,7 +12,7 @@ In this demo, the two web clients run in the same browser page (`./src/app.tsx`)
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
2. Open <http://localhost:3456/>
|
||||
2. Open <http://localhost:8080/>
|
||||
3. Click the **Start Demo** button
|
||||
|
||||
The Prover window logs the Prover's output, the Verifier logs the Verifier's output. In the console view you can see the websocket log.
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>React/Typescrip Example</title>
|
||||
<title>Web-to-Web P2P Demo</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -7,7 +7,9 @@
|
||||
"dev:server": "node ./server/index.js",
|
||||
"dev:ui": "webpack-dev-server --config webpack.js",
|
||||
"dev": "concurrently npm:dev:ui npm:dev:server",
|
||||
"build": "webpack --config webpack.js"
|
||||
"build": "webpack --config webpack.js",
|
||||
"start:ui": "webpack serve --config webpack.js",
|
||||
"test": "npm run build && npx playwright test"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@@ -45,4 +47,4 @@
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
90
demo/web-to-web-p2p/playwright.config.ts
Normal file
90
demo/web-to-web-p2p/playwright.config.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:8080',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: 'npm run start:ui',
|
||||
url: 'http://localhost:8080',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: 'wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
{
|
||||
command: 'node ./server/index.js',
|
||||
port: 3001,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
}
|
||||
]
|
||||
});
|
||||
3439
demo/web-to-web-p2p/pnpm-lock.yaml
generated
3439
demo/web-to-web-p2p/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -27,9 +27,9 @@ let proverLogs: string[] = [];
|
||||
let verifierLogs: string[] = [];
|
||||
|
||||
const p2pProxyUrl = 'ws://localhost:3001';
|
||||
const serverDns = 'swapi.dev';
|
||||
const serverDns = 'raw.githubusercontent.com';
|
||||
const webSocketProxy = `wss://notary.pse.dev/proxy?token=${serverDns}`;
|
||||
const requestUrl = `https://swapi.dev/api/people/1`;
|
||||
const requestUrl = `https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.13/crates/server-fixture/server/src/data/1kb.json`;
|
||||
|
||||
function App(): ReactElement {
|
||||
const [ready, setReady] = useState(false);
|
||||
@@ -40,7 +40,7 @@ function App(): ReactElement {
|
||||
// Initialize TLSNotary
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await init({ loggingLevel: 'Debug' });
|
||||
await init({ loggingLevel: 'Info' });
|
||||
setReady(true);
|
||||
})();
|
||||
}, []);
|
||||
@@ -96,11 +96,14 @@ function App(): ReactElement {
|
||||
addProverLog('Instantiate Prover class');
|
||||
const prover: TProver = await new Prover({
|
||||
serverDns: serverDns,
|
||||
maxRecvData: 2000
|
||||
});
|
||||
addProverLog('Prover class instantiated');
|
||||
|
||||
addVerifierLog('Instantiate Verifier class');
|
||||
const verifier: TVerifier = await new Verifier({});
|
||||
const verifier: TVerifier = await new Verifier({
|
||||
maxRecvData: 2000
|
||||
});
|
||||
addVerifierLog('Verifier class instantiated');
|
||||
|
||||
addVerifierLog('Connect verifier to p2p proxy');
|
||||
@@ -190,22 +193,22 @@ function App(): ReactElement {
|
||||
`${recvHeaders[14]}: ${recvHeaders[15]}`,
|
||||
`${recvHeaders[16]}: ${recvHeaders[17]}`,
|
||||
`${recvHeaders[18]}: ${recvHeaders[19]}`,
|
||||
`"name":"${body.name}"`,
|
||||
`"gender":"${body.gender}"`,
|
||||
`"name": "${body.information.name}"`,
|
||||
`"street": "${body.information.address.street}"`,
|
||||
],
|
||||
Buffer.from(recv).toString('utf-8'),
|
||||
),
|
||||
],
|
||||
};
|
||||
await prover.reveal(commit);
|
||||
await prover.reveal({ ...commit, server_identity: false });
|
||||
addProverLog('Data revealed to verifier');
|
||||
|
||||
const result = await verified;
|
||||
addVerifierLog('Verification completed');
|
||||
|
||||
const t = new Transcript({
|
||||
sent: result.transcript.sent,
|
||||
recv: result.transcript.recv,
|
||||
sent: result.transcript?.sent || [],
|
||||
recv: result.transcript?.recv || [],
|
||||
});
|
||||
|
||||
addVerifierLog('Verified data:');
|
||||
@@ -222,12 +225,12 @@ function App(): ReactElement {
|
||||
This demo showcases peer-to-peer communication between a web prover
|
||||
and a web verifier using TLSNotary. The prover fetches data from{' '}
|
||||
<a
|
||||
href="https://swapi.dev"
|
||||
href="https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.13/crates/server-fixture/server/src/data/1kb.json"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
swapi.dev
|
||||
our GitHub repository
|
||||
</a>{' '}
|
||||
and proves it to the verifier.
|
||||
</p>
|
||||
@@ -240,7 +243,8 @@ function App(): ReactElement {
|
||||
{proverMessages.map((m, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 text-slate-600 break-words"
|
||||
data-testid="prover-data"
|
||||
className="px-3 py-1 text-slate-600 break-all"
|
||||
>
|
||||
{m}
|
||||
</span>
|
||||
@@ -254,7 +258,8 @@ function App(): ReactElement {
|
||||
{verifierMessages.map((m, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 text-slate-600 break-words"
|
||||
data-testid="verifier-data"
|
||||
className="px-3 py-1 text-slate-600 break-all"
|
||||
>
|
||||
{m}
|
||||
</span>
|
||||
@@ -268,7 +273,7 @@ function App(): ReactElement {
|
||||
disabled={!ready || started}
|
||||
onClick={start}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div data-testid="start" className="flex items-center">
|
||||
{ready && !started ? (
|
||||
<>Start Demo</>
|
||||
) : (
|
||||
|
||||
24
demo/web-to-web-p2p/tests/demo-flow.spec.ts
Normal file
24
demo/web-to-web-p2p/tests/demo-flow.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/Web-to-Web P2P Demo/)
|
||||
});
|
||||
|
||||
test('run web-to-web p2p demo', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.getByTestId('start').click();
|
||||
|
||||
await expect(page.getByTestId('start')).toContainText('Start Demo', { timeout: 60000 });
|
||||
|
||||
const proverMessages = await page.getByTestId('prover-data').allTextContents();
|
||||
expect(proverMessages.some(text => text.includes('Transcript received'))).toBe(true);
|
||||
// console.log('Verifier Messages:', proverMessages);
|
||||
expect(proverMessages.some(text => text.includes('"name": "John Doe",'))).toBe(true);
|
||||
expect(proverMessages.some(text => text.includes('"address": {'))).toBe(true);
|
||||
|
||||
const verifierMessages = await page.getByTestId('verifier-data').allTextContents();
|
||||
expect(verifierMessages.some(text => text.includes('Verification completed'))).toBe(true);
|
||||
expect(verifierMessages.some(text => text.includes('***"name": "John Doe"*************************"street": "123 Elm Street"***'))).toBe(true);
|
||||
});
|
||||
1265
package-lock.json
generated
1265
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-js",
|
||||
"version": "0.1.0-alpha.9",
|
||||
"version": "0.1.0-alpha.13.0",
|
||||
"description": "",
|
||||
"repository": "https://github.com/tlsnotary/tlsn-js",
|
||||
"main": "build/lib.js",
|
||||
@@ -16,20 +16,18 @@
|
||||
"build:src": "webpack --config webpack.build.config.js",
|
||||
"build:types": "tsc --project tsconfig.compile.json",
|
||||
"build:lib": "NODE_ENV=production concurrently npm:build:src npm:build:types",
|
||||
"build:wasm": "sh tlsn-wasm/build.sh v0.1.0-alpha.9",
|
||||
"build:wasm": "sh tlsn-wasm/build.sh dev",
|
||||
"build": "npm run build:lib",
|
||||
"watch:dev": "webpack --config webpack.web.dev.config.js --watch",
|
||||
"dev": "concurrently npm:watch:dev npm:serve:test",
|
||||
"lint:eslint": "eslint . --fix",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint": "concurrently npm:lint:tsc npm:lint:eslint",
|
||||
"run:spec": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha -r ts-node/register 'test/specs/*.ts'",
|
||||
"run:e2e": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha -r ts-node/register 'test/testRunner.ts'",
|
||||
"test": "npm run build:test && npm run run:e2e",
|
||||
"notary": "docker run --platform=linux/amd64 -p 7047:7047 --rm ghcr.io/tlsnotary/tlsn/notary-server:v0.1.0-alpha.9 notary-server --tls-enabled=false"
|
||||
"test": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/serve-handler": "^6.1.4",
|
||||
"browserify": "^17.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -43,20 +41,17 @@
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"file-loader": "^5.0.2",
|
||||
"html-webpack-plugin": "~5.3.2",
|
||||
"https-browserify": "^1.0.0",
|
||||
"http-parser-js": "^0.5.9",
|
||||
"https-browserify": "^1.0.0",
|
||||
"image-webpack-loader": "^6.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mocha": "^10.2.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"prettier": "^3.0.2",
|
||||
"process": "^0.11.10",
|
||||
"puppeteer": "^24.1.0",
|
||||
"serve": "14.2.1",
|
||||
"serve-handler": "^6.1.5",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"ts-loader": "^6.2.1",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-eslint": "^7.4.0",
|
||||
@@ -71,6 +66,6 @@
|
||||
"node": ">= 16.20.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"tlsn-wasm": "^0.1.0-alpha.9"
|
||||
"tlsn-wasm": "./tlsn-wasm/pkg/"
|
||||
}
|
||||
}
|
||||
26
playwright-test/full-integration.spec.ts
Normal file
26
playwright-test/full-integration.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('full-integration', async ({ page }) => {
|
||||
// log browser console messages
|
||||
page.on('console', (msg) => {
|
||||
console.log(`[BROWSER ${msg.type().toUpperCase()}] ${msg.text()}`);
|
||||
});
|
||||
|
||||
await page.goto('/full-integration');
|
||||
|
||||
await expect(page.getByTestId('full-integration')).toHaveText(/\{.*\}/s, { timeout: 60000 });
|
||||
|
||||
const json = await page.getByTestId('full-integration').innerText();
|
||||
const { sent, recv, server_name, version, meta } = JSON.parse(json);
|
||||
|
||||
expect(version).toBe('0.1.0-alpha.13');
|
||||
expect(new URL(meta.notaryUrl!).protocol === 'http:');
|
||||
expect(server_name).toBe('raw.githubusercontent.com');
|
||||
|
||||
expect(sent).toContain('host: raw.githubusercontent.com');
|
||||
expect(sent).not.toContain('secret: test_secret');
|
||||
expect(recv).toContain('"id": 1234567890');
|
||||
expect(recv).toContain('"city": "Anytown"');
|
||||
expect(recv).toContain('"postalCode": "12345"');
|
||||
|
||||
});
|
||||
21
playwright-test/simple-verify.spec.ts
Normal file
21
playwright-test/simple-verify.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('simple verify', async ({ page }) => {
|
||||
// log browser console messages
|
||||
page.on('console', (msg) => {
|
||||
console.log(`[BROWSER ${msg.type().toUpperCase()}] ${msg.text()}`);
|
||||
});
|
||||
|
||||
await page.goto('/simple-verify');
|
||||
|
||||
await expect(page.getByTestId('simple-verify')).toHaveText(/\{.*\}/s);
|
||||
|
||||
const json = await page.getByTestId('simple-verify').innerText();
|
||||
const { sent, recv } = JSON.parse(json);
|
||||
|
||||
expect(sent).toContain('host: raw.githubusercontent.com');
|
||||
expect(recv).toContain('*******************');
|
||||
expect(recv).toContain('"city": "Anytown"');
|
||||
expect(recv).toContain('"id": 1234567890');
|
||||
expect(recv).toContain('"postalCode": "12345"');
|
||||
});
|
||||
85
playwright.config.ts
Normal file
85
playwright.config.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './playwright-test',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3001',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: 'npm run build:test && npm run serve:test',
|
||||
url: 'http://localhost:3001',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: 'wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
]
|
||||
});
|
||||
@@ -8,59 +8,52 @@ There is a simple react/typescript demo app in `./demo/react-ts-webpack`. The di
|
||||
|
||||
Since a web browser doesn't have the ability to make TCP connection, we need to use a websocket proxy server.
|
||||
|
||||
To run your own websocket proxy for `https://swapi.dev` **locally**:
|
||||
To run your own websocket proxy for `https://raw.githubusercontent.com` **locally**:
|
||||
|
||||
1. Install [websocat](https://github.com/vi/websocat):
|
||||
1. Install [wstcp](https://github.com/sile/wstcp):
|
||||
|
||||
| tool | command |
|
||||
| ------ | ------------------------------ |
|
||||
| cargo | `cargo install websocat` |
|
||||
| brew | `brew install websocat` |
|
||||
| source | https://github.com/vi/websocat |
|
||||
| Tool | Command |
|
||||
| ------ | ----------------------------- |
|
||||
| cargo | `cargo install wstcp` |
|
||||
| brew | `brew install wstcp` |
|
||||
| source | https://github.com/sile/wstcp |
|
||||
|
||||
2. Run a websocket proxy for `https://swapi.dev`:
|
||||
2. Run a websocket proxy for `https://raw.githubusercontent.com`:
|
||||
```sh
|
||||
websocat --binary -v ws-l:0.0.0.0:55688 tcp:swapi.dev:443
|
||||
wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443
|
||||
```
|
||||
|
||||
Note the `tcp:swapi.dev:443` argument on the last line, this is the server we will use in this quick start.
|
||||
Note the `raw.githubusercontent.com:443` argument on the last line, this is the server we will use in this quick start.
|
||||
|
||||
### Run a Local Notary Server <a name="local-notary"></a>
|
||||
|
||||
For this demo, we also need to run a local notary server.
|
||||
|
||||
1. Clone the TLSNotary repository:
|
||||
```shell
|
||||
git clone https://github.com/tlsnotary/tlsn.git --branch "v0.1.0-alpha.8"
|
||||
```
|
||||
2. Edit the notary server config file (`notary/server/config/config.yaml`) to turn off TLS so that the browser extension can connect to the local notary server without requiring extra steps to accept self-signed certificates in the browser.
|
||||
```yaml
|
||||
tls:
|
||||
enabled: false
|
||||
```
|
||||
3. Run the notary server:
|
||||
```shell
|
||||
cd notary/server
|
||||
cargo run --release
|
||||
```
|
||||
* Use docker
|
||||
```sh
|
||||
npm run notary
|
||||
```
|
||||
* Or, compile and run the notary server natively:
|
||||
```sh
|
||||
# Clone the TLSNotary repository:
|
||||
git clone https://github.com/tlsnotary/tlsn.git --branch "v0.1.0-alpha.12"
|
||||
cd tlsn/crates/notary/server/
|
||||
# Run the notary server
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
The notary server will now be running in the background waiting for connections.
|
||||
|
||||
## `tlsn-js` in a React/Typescript app
|
||||
|
||||
### Run the
|
||||
1. Clone the repository
|
||||
```sh
|
||||
git clone https://github.com/tlsnotary/tlsn-js
|
||||
```
|
||||
2. Compile tlns-js
|
||||
1. Compile tlns-js
|
||||
```sh
|
||||
npm i
|
||||
npm run build
|
||||
```
|
||||
2. Go to the demo folder
|
||||
```sh
|
||||
cd ./tlsn-js/demo/react-ts-webpack
|
||||
cd demo/react-ts-webpack
|
||||
```
|
||||
3. Install dependencies
|
||||
```sh
|
||||
|
||||
122
readme.md
122
readme.md
@@ -1,56 +1,56 @@
|
||||
![MIT licensed][mit-badge]
|
||||
![MIT licensed][mit-badge]
|
||||
![Apache licensed][apache-badge]
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[apache-badge]: https://img.shields.io/github/license/saltstack/salt
|
||||
|
||||
# tlsn-js
|
||||
|
||||
NPM Modules for proving and verifying using TLSNotary in the browser.
|
||||
|
||||
The prover requires a [notary-server](https://github.com/tlsnotary/notary-server) and a websocket proxy.
|
||||
NPM modules for proving and verifying using TLSNotary in the browser.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The primary purpose of `tlsn-js` is to support the development of the [TLSNotary browser extension](https://github.com/tlsnotary/tlsn-extension/).
|
||||
> `tlsn-js` is developed specifically for **browser environments** and does **not** work in Node.js.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The primary goal of `tlsn-js` is to support the development of the [TLSNotary browser extension](https://github.com/tlsnotary/tlsn-extension/).
|
||||
> **Please do not treat this as a public API (yet).**
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `tlsn-js` is developed for the usage of TLSNotary **in the Browser**. This module does not work in `nodejs`.
|
||||
|
||||
## License
|
||||
This repository is licensed under either of
|
||||
|
||||
This repository is licensed under either:
|
||||
|
||||
- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- [MIT license](http://opensource.org/licenses/MIT)
|
||||
- [MIT License](http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
...at your option.
|
||||
|
||||
## Examples
|
||||
|
||||
`tlsn-js` can be used in many different modes, depending on your use case.
|
||||
`tlsn-js` can be used in several modes depending on your use case.
|
||||
|
||||
The `./demo` folder contains three demos of `tlsn-js`:
|
||||
The `./demo` folder contains three demos:
|
||||
|
||||
* `react-ts-webpack`: create an attestation with a Notary and render the result.
|
||||
* `interactive-demo`: prove data interactively to a Verifier.
|
||||
* `web-to-web-p2p`: prove data between two peers, in the browser.
|
||||
- `react-ts-webpack`: Create an attestation with a Notary and render the result.
|
||||
- `interactive-demo`: Prove data interactively to a Verifier.
|
||||
- `web-to-web-p2p`: Prove data between two browser peers.
|
||||
|
||||
## Running a local websocket proxy
|
||||
## Running a Local WebSocket Proxy
|
||||
|
||||
In the demos, we attest data from the `https://swapi.dev` website. Because the browser does not allow for TCP connections, you need to set up a websocket proxy:
|
||||
In the demos, we attest data from `https://raw.githubusercontent.com`. Since browsers do not support raw TCP connections, a WebSocket proxy is required:
|
||||
|
||||
1. Install [websocat](https://github.com/vi/websocat):
|
||||
1. Install [wstcp](https://github.com/sile/wstcp):
|
||||
|
||||
| Tool | Command |
|
||||
| ------ | ------------------------------ |
|
||||
| cargo | `cargo install websocat` |
|
||||
| brew | `brew install websocat` |
|
||||
| source | https://github.com/vi/websocat |
|
||||
| Tool | Command |
|
||||
| ------ | ----------------------------- |
|
||||
| cargo | `cargo install wstcp` |
|
||||
| brew | `brew install wstcp` |
|
||||
| source | https://github.com/sile/wstcp |
|
||||
|
||||
2. Run a websocket proxy for `https://swapi.dev`:
|
||||
```sh
|
||||
websocat --binary -v ws-l:0.0.0.0:55688 tcp:swapi.dev:443
|
||||
```
|
||||
2. Run a WebSocket proxy for `https://raw.githubusercontent.com`:
|
||||
|
||||
```sh
|
||||
wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443
|
||||
```
|
||||
|
||||
## Install as NPM Package
|
||||
|
||||
@@ -58,23 +58,30 @@ websocat --binary -v ws-l:0.0.0.0:55688 tcp:swapi.dev:443
|
||||
npm install tlsn-js
|
||||
```
|
||||
|
||||
# Development
|
||||
## Development
|
||||
|
||||
This library is a JS wrapper for `tlsn-wasm`.
|
||||
This library wraps the `tlsn-wasm` module.
|
||||
|
||||
To work on `tlsn-wasm` and `tlsn-js` at the same time, replace the "tlsn-wasm" dependency in `package.json` with:
|
||||
To work on both `tlsn-wasm` and `tlsn-js` locally, update `package.json`:
|
||||
|
||||
```json
|
||||
"tlsn-wasm": "./tlsn-wasm/pkg"
|
||||
```
|
||||
"tlsn-wasm": "./tlsn-wasm/pkg"
|
||||
```
|
||||
and run `npm run build:wasm` to build `tlsn-wasm` locally.
|
||||
|
||||
Next, run:
|
||||
Then build `tlsn-wasm`:
|
||||
|
||||
```sh
|
||||
npm run build:wasm
|
||||
```
|
||||
|
||||
Next:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run test
|
||||
```
|
||||
|
||||
Note: if you want to switch back to a build with the version from npm, make sure to reset/remove `package-lock.json`, or it will keep using the local link.
|
||||
> ℹ️ To switch back to the npm-published version of `tlsn-wasm`, delete or reset `package-lock.json` to remove the local path reference.
|
||||
|
||||
## Build for NPM
|
||||
|
||||
@@ -83,9 +90,42 @@ npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Adding a new test
|
||||
1. Create a new `new-test.spec.ts` file in the `test/` directory.
|
||||
2. Add your spec file to the entry object in `webpack.web.dev.config.js`.
|
||||
3. Add a new `div` block to `test/test.ejs` like this: `<div>Testing "new-test":<div id="new-test"></div></div>`. The `div` id must be the same as the filename.
|
||||
## Testing
|
||||
|
||||
Testing is slightly complex due to the need for browser-based workers.
|
||||
|
||||
- Tests live in the `test/` directory.
|
||||
- The `tests/` directory contains a Playwright test runner that opens a Chromium browser and runs the actual test page.
|
||||
|
||||
Some tests require a running Notary. You can start one via Docker:
|
||||
|
||||
```sh
|
||||
npm run notary
|
||||
```
|
||||
|
||||
### Adding a New `tlsn-js` Test
|
||||
|
||||
1. Create a `new-test.spec.ts` file in the `test/` directory.
|
||||
2. Add your spec file to the `entry` object in `webpack.web.dev.config.js`.
|
||||
3. Create a corresponding `new-test.spec.ts` file in the `playwright-test/` directory.
|
||||
|
||||
4. Add an `expect()` call for it in `tests/test.spec.ts`.
|
||||
|
||||
### Testing the Demos
|
||||
|
||||
Playwright is also used to test the demos.
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run test
|
||||
```
|
||||
|
||||
- View tests in the browser:
|
||||
```sh
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
- Debug tests:
|
||||
```sh
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
313
src/lib.ts
313
src/lib.ts
@@ -2,24 +2,20 @@ import initWasm, {
|
||||
initialize,
|
||||
LoggingLevel,
|
||||
LoggingConfig,
|
||||
Attestation as WasmAttestation,
|
||||
Secrets as WasmSecrets,
|
||||
type Commit,
|
||||
type Reveal,
|
||||
Verifier as WasmVerifier,
|
||||
Prover as WasmProver,
|
||||
type ProverConfig,
|
||||
type Method,
|
||||
NetworkSetting,
|
||||
VerifierConfig,
|
||||
VerifierOutput,
|
||||
VerifyingKey,
|
||||
Presentation as WasmPresentation,
|
||||
build_presentation,
|
||||
ConnectionInfo,
|
||||
PartialTranscript,
|
||||
} from 'tlsn-wasm';
|
||||
import { arrayToHex, expect, headerToMap, hexToArray } from './utils';
|
||||
import { PresentationJSON } from './types';
|
||||
import { PresentationJSON, } from './types';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Transcript, subtractRanges, mapStringToRange } from './transcript';
|
||||
|
||||
@@ -67,89 +63,27 @@ export class Prover {
|
||||
#verifierUrl?: string;
|
||||
#websocketProxyUrl?: string;
|
||||
|
||||
static async notarize(options: {
|
||||
url: string;
|
||||
notaryUrl: string;
|
||||
websocketProxyUrl: string;
|
||||
method?: Method;
|
||||
headers?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
body?: unknown;
|
||||
maxSentData?: number;
|
||||
maxRecvData?: number;
|
||||
maxRecvDataOnline?: number;
|
||||
deferDecryptionFromStart?: boolean;
|
||||
commit?: Commit;
|
||||
}): Promise<PresentationJSON> {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
headers = {},
|
||||
body,
|
||||
maxSentData = 1024,
|
||||
maxRecvData = 1024,
|
||||
maxRecvDataOnline,
|
||||
deferDecryptionFromStart,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
commit: _commit,
|
||||
} = options;
|
||||
const hostname = new URL(url).hostname;
|
||||
const notary = NotaryServer.from(notaryUrl);
|
||||
const prover = new WasmProver({
|
||||
server_name: hostname,
|
||||
max_sent_data: maxSentData,
|
||||
max_recv_data: maxRecvData,
|
||||
max_recv_data_online: maxRecvDataOnline,
|
||||
defer_decryption_from_start: deferDecryptionFromStart,
|
||||
});
|
||||
|
||||
await prover.setup(await notary.sessionUrl(maxSentData, maxRecvData));
|
||||
|
||||
const headerMap = Prover.getHeaderMap(url, body, headers);
|
||||
|
||||
await prover.send_request(websocketProxyUrl + `?token=${hostname}`, {
|
||||
uri: url,
|
||||
method,
|
||||
headers: headerMap,
|
||||
body,
|
||||
});
|
||||
|
||||
const transcript = prover.transcript();
|
||||
|
||||
const commit = _commit || {
|
||||
sent: [{ start: 0, end: transcript.sent.length }],
|
||||
recv: [{ start: 0, end: transcript.recv.length }],
|
||||
};
|
||||
|
||||
const { attestation, secrets } = await prover.notarize(commit);
|
||||
|
||||
const presentation = build_presentation(attestation, secrets, commit);
|
||||
|
||||
return {
|
||||
version: '0.1.0-alpha.9',
|
||||
data: arrayToHex(presentation.serialize()),
|
||||
meta: {
|
||||
notaryUrl: notary.normalizeUrl(),
|
||||
websocketProxyUrl: websocketProxyUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor(config: {
|
||||
serverDns: string;
|
||||
maxSentData?: number;
|
||||
maxSentRecords?: number,
|
||||
maxRecvData?: number;
|
||||
maxRecvDataOnline?: number;
|
||||
maxRecvRecordsOnline?: number,
|
||||
deferDecryptionFromStart?: boolean;
|
||||
network?: NetworkSetting
|
||||
clientAuth?: [number[][], number[]] | undefined,
|
||||
}) {
|
||||
this.#config = {
|
||||
server_name: config.serverDns,
|
||||
max_recv_data: config.maxRecvData || 1024,
|
||||
max_sent_data: config.maxSentData || 1024,
|
||||
max_sent_records: config.maxSentRecords,
|
||||
max_recv_data: config.maxRecvData || 1024,
|
||||
max_recv_data_online: config.maxRecvDataOnline,
|
||||
max_recv_records_online: config.maxRecvRecordsOnline,
|
||||
defer_decryption_from_start: config.deferDecryptionFromStart,
|
||||
network: config.network || 'Bandwidth',
|
||||
client_auth: config.clientAuth
|
||||
};
|
||||
this.#prover = new WasmProver(this.#config);
|
||||
}
|
||||
@@ -233,27 +167,6 @@ export class Prover {
|
||||
};
|
||||
}
|
||||
|
||||
async notarize(commit?: Commit): Promise<{
|
||||
attestation: string;
|
||||
secrets: string;
|
||||
notaryUrl?: string;
|
||||
websocketProxyUrl?: string;
|
||||
}> {
|
||||
const transcript = await this.transcript();
|
||||
const output = await this.#prover.notarize(
|
||||
commit || {
|
||||
sent: [{ start: 0, end: transcript.sent.length }],
|
||||
recv: [{ start: 0, end: transcript.recv.length }],
|
||||
},
|
||||
);
|
||||
return {
|
||||
attestation: arrayToHex(output.attestation.serialize()),
|
||||
secrets: arrayToHex(output.secrets.serialize()),
|
||||
notaryUrl: this.#verifierUrl,
|
||||
websocketProxyUrl: this.#websocketProxyUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async reveal(reveal: Reveal) {
|
||||
return this.#prover.reveal(reveal);
|
||||
}
|
||||
@@ -263,10 +176,12 @@ export class Verifier {
|
||||
#config: VerifierConfig;
|
||||
#verifier: WasmVerifier;
|
||||
|
||||
constructor(config: { maxSentData?: number; maxRecvData?: number }) {
|
||||
constructor(config: { maxSentData?: number; maxRecvData?: number; maxSentRecords?: number; maxRecvRecordsOnline?: number }) {
|
||||
this.#config = {
|
||||
max_recv_data: config.maxRecvData || 1024,
|
||||
max_sent_data: config.maxSentData || 1024,
|
||||
max_sent_records: config.maxSentRecords,
|
||||
max_recv_records_online: config.maxRecvRecordsOnline,
|
||||
};
|
||||
this.#verifier = new WasmVerifier(this.#config);
|
||||
}
|
||||
@@ -280,205 +195,6 @@ export class Verifier {
|
||||
}
|
||||
}
|
||||
|
||||
export class Presentation {
|
||||
#presentation: WasmPresentation;
|
||||
#notaryUrl?: string;
|
||||
#websocketProxyUrl?: string;
|
||||
|
||||
constructor(
|
||||
params:
|
||||
| {
|
||||
attestationHex: string;
|
||||
secretsHex: string;
|
||||
notaryUrl?: string;
|
||||
websocketProxyUrl?: string;
|
||||
reveal?: Reveal;
|
||||
}
|
||||
| string,
|
||||
) {
|
||||
if (typeof params === 'string') {
|
||||
this.#presentation = WasmPresentation.deserialize(hexToArray(params));
|
||||
} else {
|
||||
const attestation = WasmAttestation.deserialize(
|
||||
hexToArray(params.attestationHex),
|
||||
);
|
||||
const secrets = WasmSecrets.deserialize(hexToArray(params.secretsHex));
|
||||
const transcript = secrets.transcript();
|
||||
this.#presentation = build_presentation(
|
||||
attestation,
|
||||
secrets,
|
||||
params.reveal || {
|
||||
sent: [{ start: 0, end: transcript.sent.length }],
|
||||
recv: [{ start: 0, end: transcript.recv.length }],
|
||||
},
|
||||
);
|
||||
this.#websocketProxyUrl = params.websocketProxyUrl;
|
||||
this.#notaryUrl = params.notaryUrl;
|
||||
}
|
||||
}
|
||||
|
||||
async free() {
|
||||
return this.#presentation.free();
|
||||
}
|
||||
|
||||
async serialize() {
|
||||
return arrayToHex(this.#presentation.serialize());
|
||||
}
|
||||
|
||||
async verifyingKey() {
|
||||
return this.#presentation.verifying_key();
|
||||
}
|
||||
|
||||
async json(): Promise<PresentationJSON> {
|
||||
return {
|
||||
version: '0.1.0-alpha.9',
|
||||
data: await this.serialize(),
|
||||
meta: {
|
||||
notaryUrl: this.#notaryUrl
|
||||
? NotaryServer.from(this.#notaryUrl).normalizeUrl()
|
||||
: '',
|
||||
websocketProxyUrl: this.#websocketProxyUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async verify(): Promise<VerifierOutput> {
|
||||
const {
|
||||
server_name = '',
|
||||
connection_info,
|
||||
transcript = {
|
||||
sent: [],
|
||||
recv: [],
|
||||
recv_authed: [],
|
||||
sent_authed: [],
|
||||
},
|
||||
} = this.#presentation.verify();
|
||||
|
||||
return {
|
||||
server_name: server_name,
|
||||
connection_info,
|
||||
transcript,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Attestation {
|
||||
#attestation: WasmAttestation;
|
||||
|
||||
constructor(attestationHex: string) {
|
||||
this.#attestation = WasmAttestation.deserialize(hexToArray(attestationHex));
|
||||
}
|
||||
|
||||
async free() {
|
||||
return this.#attestation.free();
|
||||
}
|
||||
|
||||
async verifyingKey() {
|
||||
return this.#attestation.verifying_key();
|
||||
}
|
||||
|
||||
async serialize() {
|
||||
return this.#attestation.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
export class Secrets {
|
||||
#secrets: WasmSecrets;
|
||||
|
||||
constructor(secretsHex: string) {
|
||||
this.#secrets = WasmSecrets.deserialize(hexToArray(secretsHex));
|
||||
}
|
||||
|
||||
async free() {
|
||||
return this.#secrets.free();
|
||||
}
|
||||
|
||||
async serialize() {
|
||||
return this.#secrets.serialize();
|
||||
}
|
||||
|
||||
async transcript() {
|
||||
return this.#secrets.transcript();
|
||||
}
|
||||
}
|
||||
|
||||
export class NotaryServer {
|
||||
#url: string;
|
||||
|
||||
static from(url: string) {
|
||||
return new NotaryServer(url);
|
||||
}
|
||||
|
||||
constructor(url: string) {
|
||||
this.#url = url;
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
async publicKey(encoding: 'pem' | 'hex' = 'hex'): Promise<string> {
|
||||
const res = await fetch(this.#url + '/info');
|
||||
const { publicKey } = await res.json();
|
||||
expect(
|
||||
typeof publicKey === 'string' && !!publicKey.length,
|
||||
'invalid public key',
|
||||
);
|
||||
|
||||
if (encoding === 'pem') {
|
||||
return publicKey!;
|
||||
}
|
||||
|
||||
return Buffer.from(
|
||||
publicKey!
|
||||
.replace('-----BEGIN PUBLIC KEY-----', '')
|
||||
.replace('-----END PUBLIC KEY-----', '')
|
||||
.replace(/\n/g, ''),
|
||||
'base64',
|
||||
)
|
||||
.slice(23)
|
||||
.toString('hex');
|
||||
}
|
||||
|
||||
normalizeUrl() {
|
||||
const url = new URL(this.#url);
|
||||
let protocol;
|
||||
|
||||
if (url.protocol === 'https:' || url.protocol === 'http:') {
|
||||
protocol = url.protocol;
|
||||
} else {
|
||||
protocol = url.protocol === 'wss:' ? 'https:' : 'http:';
|
||||
}
|
||||
|
||||
return `${protocol}//${url.host}`;
|
||||
}
|
||||
|
||||
async sessionUrl(
|
||||
maxSentData?: number,
|
||||
maxRecvData?: number,
|
||||
): Promise<string> {
|
||||
const resp = await fetch(`${this.#url}/session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientType: 'Websocket',
|
||||
maxRecvData,
|
||||
maxSentData,
|
||||
}),
|
||||
});
|
||||
const { sessionId } = await resp.json();
|
||||
expect(
|
||||
typeof sessionId === 'string' && !!sessionId.length,
|
||||
'invalid session id',
|
||||
);
|
||||
const url = new URL(this.#url);
|
||||
const protocol = url.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const pathname = url.pathname;
|
||||
return `${protocol}://${url.host}${pathname === '/' ? '' : pathname}/notarize?sessionId=${sessionId!}`;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
type LoggingLevel,
|
||||
@@ -488,7 +204,6 @@ export {
|
||||
type Reveal,
|
||||
type ProverConfig,
|
||||
type VerifierConfig,
|
||||
type VerifyingKey,
|
||||
type VerifierOutput,
|
||||
type ConnectionInfo,
|
||||
type PartialTranscript,
|
||||
|
||||
@@ -17,19 +17,11 @@ export class Transcript {
|
||||
}
|
||||
|
||||
recv(redactedSymbol = '*') {
|
||||
return this.#recv.reduce((recv: string, num) => {
|
||||
recv =
|
||||
recv + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
|
||||
return recv;
|
||||
}, '');
|
||||
return bytesToUtf8(substituteRedactions(this.#recv, redactedSymbol));
|
||||
}
|
||||
|
||||
sent(redactedSymbol = '*') {
|
||||
return this.#sent.reduce((sent: string, num) => {
|
||||
sent =
|
||||
sent + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
|
||||
return sent;
|
||||
}, '');
|
||||
return bytesToUtf8(substituteRedactions(this.#sent, redactedSymbol));
|
||||
}
|
||||
|
||||
text = (redactedSymbol = '*') => {
|
||||
@@ -101,3 +93,15 @@ function indexOfString(str: string, substr: string): number {
|
||||
function bytesSize(str: string): number {
|
||||
return Buffer.from(str).byteLength;
|
||||
}
|
||||
|
||||
function bytesToUtf8(array: number[]): string {
|
||||
return Buffer.from(array).toString("utf8");
|
||||
}
|
||||
|
||||
function substituteRedactions(
|
||||
array: number[],
|
||||
redactedSymbol: string = "*",
|
||||
): number[] {
|
||||
const replaceCharByte = redactedSymbol.charCodeAt(0);
|
||||
return array.map((byte) => (byte === 0 ? replaceCharByte : byte));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type CommitData = {
|
||||
};
|
||||
|
||||
export type PresentationJSON = {
|
||||
version: '0.1.0-alpha.7' | '0.1.0-alpha.8' | '0.1.0-alpha.9';
|
||||
version: '0.1.0-alpha.7' | '0.1.0-alpha.8' | '0.1.0-alpha.9' | '0.1.0-alpha.10' | '0.1.0-alpha.11' | '0.1.0-alpha.12' | '0.1.0-alpha.13';
|
||||
data: string;
|
||||
meta: {
|
||||
notaryUrl?: string;
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
mapStringToRange,
|
||||
subtractRanges,
|
||||
Transcript,
|
||||
Reveal,
|
||||
} from '../../src/lib';
|
||||
import * as Comlink from 'comlink';
|
||||
import { assert } from '../utils';
|
||||
import { HTTPParser } from 'http-parser-js';
|
||||
|
||||
const { init, Prover, Presentation }: any = Comlink.wrap(
|
||||
@@ -23,13 +23,17 @@ const { init, Prover, Presentation }: any = Comlink.wrap(
|
||||
console.log('test start');
|
||||
console.time('prove');
|
||||
const prover = (await new Prover({
|
||||
id: 'test',
|
||||
serverDns: 'swapi.dev',
|
||||
serverDns: 'raw.githubusercontent.com',
|
||||
maxRecvData: 1700,
|
||||
network: "Bandwidth",
|
||||
})) as _Prover;
|
||||
const notary = NotaryServer.from('http://127.0.0.1:7047');
|
||||
await prover.setup(await notary.sessionUrl());
|
||||
await prover.sendRequest('wss://notary.pse.dev/proxy?token=swapi.dev', {
|
||||
url: 'https://swapi.dev/api/people/1',
|
||||
// const websocketProxyUrl = 'wss://notary.pse.dev/proxy?token=raw.githubusercontent.com';
|
||||
const websocketProxyUrl = 'ws://127.0.0.1:55688';
|
||||
|
||||
await prover.sendRequest(websocketProxyUrl, {
|
||||
url: 'https://raw.githubusercontent.com/tlsnotary/tlsn/refs/heads/main/crates/server-fixture/server/src/data/protected_data.json',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
secret: 'test_secret',
|
||||
@@ -65,9 +69,10 @@ const { init, Prover, Presentation }: any = Comlink.wrap(
|
||||
`${recvHeaders[14]}: ${recvHeaders[15]}`,
|
||||
`${recvHeaders[16]}: ${recvHeaders[17]}`,
|
||||
`${recvHeaders[18]}: ${recvHeaders[19]}`,
|
||||
`"name":"${body.name}"`,
|
||||
`"hair_color":"${body.hair_color}"`,
|
||||
`"skin_color":"${body.skin_color}"`,
|
||||
`"id": ${body.id}`,
|
||||
`"city": "${body.information.address.city}"`,
|
||||
`"postalCode": "12345"`,
|
||||
|
||||
],
|
||||
Buffer.from(recv).toString('utf-8'),
|
||||
),
|
||||
@@ -75,18 +80,21 @@ const { init, Prover, Presentation }: any = Comlink.wrap(
|
||||
};
|
||||
console.log(commit);
|
||||
const notarizationOutput = await prover.notarize(commit);
|
||||
const reveal: Reveal = {
|
||||
...commit,
|
||||
server_identity: false,
|
||||
};
|
||||
const presentation = (await new Presentation({
|
||||
attestationHex: notarizationOutput.attestation,
|
||||
secretsHex: notarizationOutput.secrets,
|
||||
reveal: commit,
|
||||
reveal: reveal,
|
||||
notaryUrl: notary.url,
|
||||
websocketProxyUrl: 'wss://notary.pse.dev/proxy',
|
||||
})) as _Presentation;
|
||||
console.log('presentation:', await presentation.serialize());
|
||||
console.timeEnd('prove');
|
||||
const json = await presentation.json();
|
||||
assert(json.version === '0.1.0-alpha.9');
|
||||
assert(new URL(json.meta.notaryUrl!).protocol === 'http:');
|
||||
|
||||
|
||||
console.time('verify');
|
||||
const { transcript: partialTranscript, server_name } =
|
||||
@@ -101,21 +109,23 @@ const { init, Prover, Presentation }: any = Comlink.wrap(
|
||||
});
|
||||
const sentStr = t.sent();
|
||||
const recvStr = t.recv();
|
||||
assert(sentStr.includes('host: swapi.dev'));
|
||||
assert(!sentStr.includes('secret: test_secret'));
|
||||
assert(recvStr.includes('"name":"Luke Skywalker"'));
|
||||
assert(recvStr.includes('"hair_color":"blond"'));
|
||||
assert(recvStr.includes('"skin_color":"fair"'));
|
||||
assert(server_name === 'swapi.dev');
|
||||
|
||||
console.log("Sent:", sentStr);
|
||||
console.log("Received:", recvStr);
|
||||
|
||||
// @ts-ignore
|
||||
document.getElementById('full-integration-swapi').textContent = 'OK';
|
||||
document.getElementById('full-integration').textContent = JSON.stringify({
|
||||
sent: sentStr,
|
||||
recv: recvStr,
|
||||
version: json.version,
|
||||
meta: json.meta,
|
||||
server_name
|
||||
}, null, 2);
|
||||
} catch (err) {
|
||||
console.log('caught error from wasm');
|
||||
console.error(err);
|
||||
|
||||
// @ts-ignore
|
||||
document.getElementById('full-integration-swapi').textContent = err.message;
|
||||
document.getElementById('full-integration').textContent = err.message;
|
||||
}
|
||||
})();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,90 +0,0 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { Transcript } from '../../src/transcript';
|
||||
|
||||
describe('Transcript parsing', () => {
|
||||
it('should parse transcript correctly', async () => {
|
||||
const transcript = new Transcript({ sent: swapiSent, recv: swapiRecv });
|
||||
assert.strictEqual(
|
||||
Buffer.from(transcript.raw.sent).toString('utf-8'),
|
||||
'GET https://swapi.dev/api/people/1 HTTP/1.1\r\nconnection: close\r\ncontent-length: 25\r\ncontent-type: application/json\r\nhost: swapi.dev\r\n\r\n{"hello":"world","one":1}',
|
||||
);
|
||||
assert.strictEqual(
|
||||
Buffer.from(transcript.raw.recv).toString('utf-8'),
|
||||
'HTTP/1.1 200 OK\r\nServer: nginx/1.16.1\r\nDate: Fri, 07 Feb 2025 07:37:11 GMT\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: close\r\nVary: Accept, Cookie\r\nX-Frame-Options: SAMEORIGIN\r\nETag: \"ee398610435c328f4d0a4e1b0d2f7bbc\"\r\nAllow: GET, HEAD, OPTIONS\r\nStrict-Transport-Security: max-age=15768000\r\n\r\n287\r\n{\"name\":\"Luke Skywalker\",\"height\":\"172\",\"mass\":\"77\",\"hair_color\":\"blond\",\"skin_color\":\"fair\",\"eye_color\":\"blue\",\"birth_year\":\"19BBY\",\"gender\":\"male\",\"homeworld\":\"https://swapi.dev/api/planets/1/\",\"films\":[\"https://swapi.dev/api/films/1/\",\"https://swapi.dev/api/films/2/\",\"https://swapi.dev/api/films/3/\",\"https://swapi.dev/api/films/6/\"],\"species\":[],\"vehicles\":[\"https://swapi.dev/api/vehicles/14/\",\"https://swapi.dev/api/vehicles/30/\"],\"starships\":[\"https://swapi.dev/api/starships/12/\",\"https://swapi.dev/api/starships/22/\"],\"created\":\"2014-12-09T13:50:51.644000Z\",\"edited\":\"2014-12-20T21:17:56.891000Z\",\"url\":\"https://swapi.dev/api/people/1/\"}\r\n0\r\n\r\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const swapiRecv = [
|
||||
72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75, 13, 10, 83, 101,
|
||||
114, 118, 101, 114, 58, 32, 110, 103, 105, 110, 120, 47, 49, 46, 49, 54, 46,
|
||||
49, 13, 10, 68, 97, 116, 101, 58, 32, 70, 114, 105, 44, 32, 48, 55, 32, 70,
|
||||
101, 98, 32, 50, 48, 50, 53, 32, 48, 55, 58, 51, 55, 58, 49, 49, 32, 71, 77,
|
||||
84, 13, 10, 67, 111, 110, 116, 101, 110, 116, 45, 84, 121, 112, 101, 58, 32,
|
||||
97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 106, 115, 111, 110,
|
||||
13, 10, 84, 114, 97, 110, 115, 102, 101, 114, 45, 69, 110, 99, 111, 100, 105,
|
||||
110, 103, 58, 32, 99, 104, 117, 110, 107, 101, 100, 13, 10, 67, 111, 110, 110,
|
||||
101, 99, 116, 105, 111, 110, 58, 32, 99, 108, 111, 115, 101, 13, 10, 86, 97,
|
||||
114, 121, 58, 32, 65, 99, 99, 101, 112, 116, 44, 32, 67, 111, 111, 107, 105,
|
||||
101, 13, 10, 88, 45, 70, 114, 97, 109, 101, 45, 79, 112, 116, 105, 111, 110,
|
||||
115, 58, 32, 83, 65, 77, 69, 79, 82, 73, 71, 73, 78, 13, 10, 69, 84, 97, 103,
|
||||
58, 32, 34, 101, 101, 51, 57, 56, 54, 49, 48, 52, 51, 53, 99, 51, 50, 56, 102,
|
||||
52, 100, 48, 97, 52, 101, 49, 98, 48, 100, 50, 102, 55, 98, 98, 99, 34, 13,
|
||||
10, 65, 108, 108, 111, 119, 58, 32, 71, 69, 84, 44, 32, 72, 69, 65, 68, 44,
|
||||
32, 79, 80, 84, 73, 79, 78, 83, 13, 10, 83, 116, 114, 105, 99, 116, 45, 84,
|
||||
114, 97, 110, 115, 112, 111, 114, 116, 45, 83, 101, 99, 117, 114, 105, 116,
|
||||
121, 58, 32, 109, 97, 120, 45, 97, 103, 101, 61, 49, 53, 55, 54, 56, 48, 48,
|
||||
48, 13, 10, 13, 10, 50, 56, 55, 13, 10, 123, 34, 110, 97, 109, 101, 34, 58,
|
||||
34, 76, 117, 107, 101, 32, 83, 107, 121, 119, 97, 108, 107, 101, 114, 34, 44,
|
||||
34, 104, 101, 105, 103, 104, 116, 34, 58, 34, 49, 55, 50, 34, 44, 34, 109, 97,
|
||||
115, 115, 34, 58, 34, 55, 55, 34, 44, 34, 104, 97, 105, 114, 95, 99, 111, 108,
|
||||
111, 114, 34, 58, 34, 98, 108, 111, 110, 100, 34, 44, 34, 115, 107, 105, 110,
|
||||
95, 99, 111, 108, 111, 114, 34, 58, 34, 102, 97, 105, 114, 34, 44, 34, 101,
|
||||
121, 101, 95, 99, 111, 108, 111, 114, 34, 58, 34, 98, 108, 117, 101, 34, 44,
|
||||
34, 98, 105, 114, 116, 104, 95, 121, 101, 97, 114, 34, 58, 34, 49, 57, 66, 66,
|
||||
89, 34, 44, 34, 103, 101, 110, 100, 101, 114, 34, 58, 34, 109, 97, 108, 101,
|
||||
34, 44, 34, 104, 111, 109, 101, 119, 111, 114, 108, 100, 34, 58, 34, 104, 116,
|
||||
116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105, 46, 100, 101, 118, 47, 97,
|
||||
112, 105, 47, 112, 108, 97, 110, 101, 116, 115, 47, 49, 47, 34, 44, 34, 102,
|
||||
105, 108, 109, 115, 34, 58, 91, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
|
||||
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
|
||||
109, 115, 47, 49, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
|
||||
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
|
||||
109, 115, 47, 50, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
|
||||
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
|
||||
109, 115, 47, 51, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
|
||||
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
|
||||
109, 115, 47, 54, 47, 34, 93, 44, 34, 115, 112, 101, 99, 105, 101, 115, 34,
|
||||
58, 91, 93, 44, 34, 118, 101, 104, 105, 99, 108, 101, 115, 34, 58, 91, 34,
|
||||
104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105, 46, 100, 101,
|
||||
118, 47, 97, 112, 105, 47, 118, 101, 104, 105, 99, 108, 101, 115, 47, 49, 52,
|
||||
47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105,
|
||||
46, 100, 101, 118, 47, 97, 112, 105, 47, 118, 101, 104, 105, 99, 108, 101,
|
||||
115, 47, 51, 48, 47, 34, 93, 44, 34, 115, 116, 97, 114, 115, 104, 105, 112,
|
||||
115, 34, 58, 91, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112,
|
||||
105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 115, 116, 97, 114, 115, 104,
|
||||
105, 112, 115, 47, 49, 50, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47,
|
||||
47, 115, 119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 115, 116,
|
||||
97, 114, 115, 104, 105, 112, 115, 47, 50, 50, 47, 34, 93, 44, 34, 99, 114,
|
||||
101, 97, 116, 101, 100, 34, 58, 34, 50, 48, 49, 52, 45, 49, 50, 45, 48, 57,
|
||||
84, 49, 51, 58, 53, 48, 58, 53, 49, 46, 54, 52, 52, 48, 48, 48, 90, 34, 44,
|
||||
34, 101, 100, 105, 116, 101, 100, 34, 58, 34, 50, 48, 49, 52, 45, 49, 50, 45,
|
||||
50, 48, 84, 50, 49, 58, 49, 55, 58, 53, 54, 46, 56, 57, 49, 48, 48, 48, 90,
|
||||
34, 44, 34, 117, 114, 108, 34, 58, 34, 104, 116, 116, 112, 115, 58, 47, 47,
|
||||
115, 119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 112, 101,
|
||||
111, 112, 108, 101, 47, 49, 47, 34, 125, 13, 10, 48, 13, 10, 13, 10,
|
||||
];
|
||||
|
||||
const swapiSent = [
|
||||
71, 69, 84, 32, 104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105,
|
||||
46, 100, 101, 118, 47, 97, 112, 105, 47, 112, 101, 111, 112, 108, 101, 47, 49,
|
||||
32, 72, 84, 84, 80, 47, 49, 46, 49, 13, 10, 99, 111, 110, 110, 101, 99, 116,
|
||||
105, 111, 110, 58, 32, 99, 108, 111, 115, 101, 13, 10, 99, 111, 110, 116, 101,
|
||||
110, 116, 45, 108, 101, 110, 103, 116, 104, 58, 32, 50, 53, 13, 10, 99, 111,
|
||||
110, 116, 101, 110, 116, 45, 116, 121, 112, 101, 58, 32, 97, 112, 112, 108,
|
||||
105, 99, 97, 116, 105, 111, 110, 47, 106, 115, 111, 110, 13, 10, 104, 111,
|
||||
115, 116, 58, 32, 115, 119, 97, 112, 105, 46, 100, 101, 118, 13, 10, 13, 10,
|
||||
123, 34, 104, 101, 108, 108, 111, 34, 58, 34, 119, 111, 114, 108, 100, 34, 44,
|
||||
34, 111, 110, 101, 34, 58, 49, 125,
|
||||
];
|
||||
@@ -5,19 +5,21 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<title>tlsn-js development</title>
|
||||
<title>
|
||||
Testing <%= htmlWebpackPlugin.options.testName || 'test' %>
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
global = globalThis //<- this should be enough
|
||||
</script>
|
||||
<div>Testing "full-integration-swapi":
|
||||
<div id="full-integration-swapi"></div>
|
||||
</div>
|
||||
<div>Testing "simple-verify":
|
||||
<div id="simple-verify"></div>
|
||||
</div>
|
||||
<h1>Testing "<%= htmlWebpackPlugin.options.testName || 'unknown' %>":</h1>
|
||||
<pre>
|
||||
<div id="<%= htmlWebpackPlugin.options.testName || 'test' %>"
|
||||
data-testid="<%= htmlWebpackPlugin.options.testName || 'test' %>">
|
||||
</div>
|
||||
</pre>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,119 +0,0 @@
|
||||
import puppeteer, { Browser, LaunchOptions, Page } from 'puppeteer';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
const assert = require('assert');
|
||||
import { exec, ChildProcess } from 'node:child_process';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const timeout = 300000;
|
||||
|
||||
// puppeteer options
|
||||
let opts: LaunchOptions = {
|
||||
headless: !!process.env.HEADLESS ? true : false,
|
||||
slowMo: 100,
|
||||
timeout: timeout,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
};
|
||||
|
||||
if (process.env.CHROME_PATH) {
|
||||
opts = {
|
||||
...opts,
|
||||
executablePath: process.env.CHROME_PATH,
|
||||
};
|
||||
}
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
let server: ChildProcess;
|
||||
|
||||
const waitForNotaryServer = async () => {
|
||||
// wait for the notary server to be ready
|
||||
while (true) {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:7047/info');
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Waiting for local notary server...', error);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
};
|
||||
|
||||
// expose variables
|
||||
before(async function () {
|
||||
server = exec('serve --config ../serve.json ./test-build -l 3001');
|
||||
|
||||
await waitForNotaryServer();
|
||||
browser = await puppeteer.launch(opts);
|
||||
page = await browser.newPage();
|
||||
await page.goto('http://127.0.0.1:3001');
|
||||
});
|
||||
|
||||
// close browser and reset global variables
|
||||
after(async function () {
|
||||
console.log('Cleaning up:');
|
||||
|
||||
try {
|
||||
server.kill();
|
||||
console.log('* Stopped Test Web Server ✅');
|
||||
|
||||
if (page) {
|
||||
await page.close();
|
||||
}
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
const childProcess = browser.process();
|
||||
if (childProcess) {
|
||||
childProcess.kill(9);
|
||||
}
|
||||
console.log('* Closed browser ✅');
|
||||
|
||||
const tests = this.test?.parent?.suites.flatMap((suite) => suite.tests);
|
||||
const failed = tests!.some((test) => test.state === 'failed');
|
||||
|
||||
console.log('tests', tests);
|
||||
console.log('failed', failed);
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
process.exit(1);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
describe('tlsn-js test suite', function () {
|
||||
fs.readdirSync(path.join(__dirname, 'e2e')).forEach((file) => {
|
||||
const [id] = file.split('.');
|
||||
it(`Test ID: ${id}`, async function () {
|
||||
const content = await check(id);
|
||||
assert.strictEqual(
|
||||
content,
|
||||
'OK',
|
||||
`Test ID: ${id} - Expected 'OK' but got '${content}'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function check(testId: string): Promise<string> {
|
||||
const startTime = Date.now();
|
||||
const attemptFetchContent = async (): Promise<string> => {
|
||||
const content = await page.$eval(
|
||||
`#${testId}`,
|
||||
(el: any) => el.textContent || '',
|
||||
);
|
||||
if (content) return content;
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
if (elapsedTime >= timeout) {
|
||||
throw new Error(
|
||||
`Timeout: Failed to retrieve content for '#${testId}' within ${timeout} ms.`,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return attemptFetchContent();
|
||||
};
|
||||
return attemptFetchContent();
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function assert(expr: any, msg = 'unknown assertion error') {
|
||||
if (!Boolean(expr)) throw new Error(msg);
|
||||
}
|
||||
@@ -33,14 +33,17 @@ const rules = [
|
||||
|
||||
const rendererRules = [];
|
||||
|
||||
const entry = {
|
||||
'full-integration': path.join(__dirname, 'test', 'e2e', 'full-integration.spec.ts'),
|
||||
'simple-verify': path.join(__dirname, 'test', 'e2e', 'simple-verify.spec.ts'),
|
||||
// add more entries as needed
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
target: 'web',
|
||||
mode: isProd ? 'production' : 'development',
|
||||
entry: {
|
||||
'full-integration-swapi.spec': path.join(__dirname, 'test', 'e2e', 'full-integration-swapi.spec.ts'),
|
||||
'simple-verify': path.join(__dirname, 'test', 'e2e', 'simple-verify.spec.ts'),
|
||||
},
|
||||
entry,
|
||||
output: {
|
||||
path: __dirname + '/test-build',
|
||||
publicPath: '/',
|
||||
@@ -49,25 +52,6 @@ module.exports = [
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.png', '.svg'],
|
||||
// modules: [
|
||||
// path.resolve('./node_modules'),
|
||||
// path.resolve(__dirname, compilerOptions.baseUrl),
|
||||
// ],
|
||||
// fallback: {
|
||||
// browserify: require.resolve('browserify'),
|
||||
// stream: require.resolve('stream-browserify'),
|
||||
// path: require.resolve('path-browserify'),
|
||||
// crypto: require.resolve('crypto-browserify'),
|
||||
// os: require.resolve('os-browserify/browser'),
|
||||
// http: require.resolve('stream-http'),
|
||||
// https: require.resolve('https-browserify'),
|
||||
// assert: require.resolve('assert/'),
|
||||
// events: require.resolve('events/'),
|
||||
// 'ansi-html-community': require.resolve('ansi-html-community'),
|
||||
// 'html-entities': require.resolve('html-entities'),
|
||||
// constants: false,
|
||||
// fs: false,
|
||||
// },
|
||||
},
|
||||
module: {
|
||||
rules: [...rules, ...rendererRules],
|
||||
@@ -89,10 +73,41 @@ module.exports = [
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Generate an HTML file for each entry
|
||||
...Object.keys(entry).map(
|
||||
(name) =>
|
||||
new HtmlWebpackPlugin({
|
||||
template: './test/test.ejs',
|
||||
filename: `${name}.html`,
|
||||
chunks: [name],
|
||||
inject: true,
|
||||
testName: name,
|
||||
})
|
||||
),
|
||||
// Add an index page listing all test pages
|
||||
new HtmlWebpackPlugin({
|
||||
template: './test/test.ejs',
|
||||
filename: `index.html`,
|
||||
inject: true,
|
||||
templateContent: () => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>tlsn-js test index</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>tlsn-js test index</h1>
|
||||
<ul>
|
||||
${Object.keys(entry)
|
||||
.map(
|
||||
(name) =>
|
||||
`<li><a href="${name}.html">${name}</a></li>`
|
||||
)
|
||||
.join('\n')}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
filename: 'index.html',
|
||||
inject: false,
|
||||
}),
|
||||
],
|
||||
stats: 'minimal',
|
||||
|
||||
Reference in New Issue
Block a user