19 Commits

Author SHA1 Message Date
Hendrik Eeckhaut
8e2a944243 build: Pin transitive dependencies for tlsn alpha.12 (#115) 2025-10-02 19:45:30 +02:00
Piotr Żelazko
e6b7db5acf fix: ensure transcript decoding supports unicode (#112) 2025-09-16 21:22:34 +08:00
dan
66ec4343e8 chore: update config options + update to alpha.12 (#113)
* chore: update config options

* Alpha.12
---------

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-06-19 12:23:49 -07:00
Hendrik Eeckhaut
f51ddbf3de ci: notary server has tls disabled by default since alpha.11 (#111) 2025-06-02 11:32:32 +02:00
Hendrik Eeckhaut
1cb664b341 Alpha.11 (#109)
Co-authored-by: yuroitaki <25913766+yuroitaki@users.noreply.github.com>
2025-05-30 10:22:11 +02:00
Hendrik Eeckhaut
4cecbb5334 Use Playwright to test demos (#106)
* Run tests and demos with playwright
* ci: renamed workflow
* Improved demo readmes
* Use a separate page for each test
2025-05-28 08:50:05 +02:00
Hendrik Eeckhaut
8bc8a94948 chore: Use raw.githubusercontent.com instead of swapi for the demos (#105)
+ use a local proxy for testing
+ avoid duplicate github action runs
2025-05-12 16:34:36 +02:00
Hendrik Eeckhaut
8bf3745407 Update to tlsnotary v0.1.0-alpha.10 (#104)
* Update to tlsnotary v0.1.0-alpha.10
* Prove data from GitHub server instead of swapi in tests
* Log browser messages during test execution
2025-04-25 14:00:14 +02:00
Hendrik Eeckhaut
a418082762 chore: prepare 0.1.0-alpha.9.1 release 2025-04-03 10:05:33 +02:00
Hendrik Eeckhaut
07f2645a65 chore: update examples to alpha.9 + improve readme (#102) 2025-04-03 09:52:11 +02:00
Grzegorz Pociejewski
c70abc5eb2 Pin wasm version (#103) 2025-04-03 03:49:04 -04:00
Hendrik Eeckhaut
be717f4260 ci: correctly set auth token for npm publish 2025-03-28 08:58:25 +01:00
Hendrik Eeckhaut
0372b8e8aa Update to alpha.9 + added way to co-develop tlsn-wasm (#99) 2025-03-27 14:50:16 +01:00
Hendrik Eeckhaut
bf0114085f update readme (#98) 2025-03-26 17:06:40 +01:00
Hendrik Eeckhaut
d2ae87106f ci: use npm instead of pnpm (#100) 2025-03-17 17:41:24 +01:00
Hendrik Eeckhaut
9fad45e174 chore: run notary server in test with docker (#95) 2025-03-14 03:48:45 -04:00
tsukino
18268f716f feat: upgrade to alpha.8 (#93) 2025-03-12 12:04:58 -04:00
tsukino
91f74471c7 feat: return raw data from transcript and move parsing to client side (#92) 2025-02-28 04:28:06 -05:00
Tanner
0133efe529 Demo UI (#85)
* feat: webp2p demo ui update

* feat: webp2p demo ui update

* fix: webp2p webpack

* chore: linting

* feat: styled react-ts demo, fixed interactive demo bug

* chore: cleanup

* Improved React demo

* fix: unnessecary dependencies in package

* Improved interactive demo

* Fixed readme

* Removed unused type

---------

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-02-05 11:21:13 -08:00
74 changed files with 4888 additions and 36793 deletions

View File

@@ -29,8 +29,7 @@
"build",
"test-build",
"dev-build",
"wasm",
"utils",
"tlsn-wasm",
"webpack.config.js",
"webpack.test.config.js"
]

View File

@@ -1,50 +0,0 @@
name: build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
PUPPETEER_SKIP_DOWNLOAD: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: npm run build
- name: Lint
run: npm run lint

88
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: ci
on:
pull_request:
release:
types: [published]
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.12
ports:
- 7047:7047
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout tlsn
uses: actions/checkout@v4
with:
repository: tlsnotary/tlsn
path: tlsn-wasm/tlsn
- name: Setup Node.js
uses: actions/setup-node@v4
with:
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
- name: Use caching
uses: Swatinem/rust-cache@v2.7.7
with:
workspaces: tlsn-wasm/tlsn -> target
cache-on-failure: true
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Lint
run: npm run lint
- name: install wstcp
run: cargo install wstcp
- name: Install Chromium (Playwright)
run: npx playwright install --with-deps chromium
- name: Test
run: npm run test
- name: Determine release type (dry-run or publish)
run: |
if [[ $GITHUB_EVENT_NAME == "release" ]]; then
echo "RELEASE_MODE=publish" >> $GITHUB_ENV
else
echo "RELEASE_MODE=dry-run" >> $GITHUB_ENV
fi
- name: Dry-run release (non-release builds)
if: env.RELEASE_MODE == 'dry-run'
run: npm publish --dry-run
- name: Publish to npm (GitHub Release)
if: env.RELEASE_MODE == 'publish'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc
npm publish
rm .npmrc

54
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Tests demos
on:
pull_request:
jobs:
test:
timeout-minutes: 60
name: Tests demos
runs-on: ubuntu-latest
services:
notary-server:
image: ghcr.io/tlsnotary/tlsn/notary-server:v0.1.0-alpha.12
env:
NOTARY_SERVER__TLS__ENABLED: false
ports:
- 7047:7047
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

View File

@@ -1,77 +0,0 @@
name: test
on:
push:
branches: [ main ]
pull_request:
env:
LOCAL-NOTARY: true
LOCAL-WS: false
HEADLESS: true
PUPPETEER_SKIP_DOWNLOAD: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install stable rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
toolchain: nightly
- name: Use caching
uses: Swatinem/rust-cache@v2
with:
workspaces: wasm/prover
- 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 Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${STORE_PATH}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install nightly tool-chain
run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
- name: Install dependencies
run: pnpm install
- name: Build Test dependencies
run: npm run build:tlsn-binaries
- name: Test
run: npm run test

7
.gitignore vendored
View File

@@ -6,3 +6,10 @@ dev-build/
test-build/
./demo/node_modules
utils/tlsn
.vscode
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -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"
],
}

View File

@@ -1,38 +1,59 @@
# Test Rust Prover
# Interactive Verifier Demo
1. Start the verifier:
```bash
cd verifier-rs; cargo run --release
```
2. Run the prover:
```bash
cd prover-rs; cargo run --release
```
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.
# 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
There are two prover implementations:
- **Rust**
- **TypeScript** (browser)
The verifier is implemented in Rust.
wstcp --bind-addr 127.0.0.1:55688 swapi.dev:443
```
3. Run the prover
1. Build tlsn-js
---
## 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 i
npm install
npm run build
npm link
```
2. Build demo prover-ts
2. **Build and start the TypeScript prover demo**
```bash
cd prover-ts
npm i
npm link
npm install
npm run dev
```
3. Open <http://localhost:3456/> and click **Start Prover**
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.

View File

@@ -8,23 +8,31 @@ 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"]}
hyper = { version = "1.1", features = ["client", "http1"] }
hyper-util = { version = "0.1", features = ["full"] }
regex = "1.10.3"
tokio = {version = "1", features = [
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"] }
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.7", package = "tlsn-core" }
tlsn-prover = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.7", package = "tlsn-prover" }
tlsn-common = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.7", package = "tlsn-common" }
tlsn-core = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.12", package = "tlsn-core" }
tlsn-prover = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.12", package = "tlsn-prover" }
tlsn-common = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.12", package = "tlsn-common" }
spansy = { git = "https://github.com/tlsnotary/tlsn-utils", package = "spansy", branch = "dev" }
rangeset = "0.2.0"
# --- Transitive dependency pins (for TLSNotary alpha.12)---
aes = "=0.9.0-rc.0"
cipher = "=0.5.0-rc.0"
crypto-common = "=0.2.0-rc.3"
inout = "=0.2.0-rc.5"

View File

@@ -2,7 +2,7 @@
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.
1. Configure this prover setting via the global variables defined in [main.rs](./src/main.rs) — please ensure that the hardcoded `SERVER_URL` is the same 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

View File

@@ -2,10 +2,15 @@ use async_tungstenite::{tokio::connect_async_with_config, tungstenite::protocol:
use http_body_util::Empty;
use hyper::{body::Bytes, Request, StatusCode, Uri};
use hyper_util::rt::TokioIo;
use regex::Regex;
use rangeset::RangeSet;
use spansy::{
http::parse_response,
json::{self},
Spanned,
};
use tlsn_common::config::ProtocolConfig;
use tlsn_core::transcript::Idx;
use tlsn_prover::{state::Prove, Prover, ProverConfig};
use tlsn_core::ProveConfig;
use tlsn_prover::{Prover, ProverConfig};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::{debug, info};
@@ -23,7 +28,7 @@ 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";
const SERVER_URL: &str = "https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.12/crates/server-fixture/server/src/data/1kb.json";
#[tokio::main]
async fn main() {
@@ -38,7 +43,7 @@ async fn main() {
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,))
.uri(format!("ws://{verifier_host}:{verifier_port}/verify",))
.header("Host", verifier_host)
.header("Sec-WebSocket-Key", uuid::Uuid::new_v4().to_string())
.header("Sec-WebSocket-Version", "13")
@@ -125,38 +130,51 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(verifier_soc
assert!(response.status() == StatusCode::OK);
// Create proof for the Verifier.
let mut prover = prover_task.await.unwrap().unwrap().start_prove();
let mut prover = prover_task.await.unwrap().unwrap();
let idx_sent = redact_and_reveal_sent_data(&mut prover);
let idx_recv = redact_and_reveal_received_data(&mut prover);
let mut builder: tlsn_core::ProveConfigBuilder<'_> = ProveConfig::builder(prover.transcript());
// Reveal parts of the transcript
prover.prove_transcript(idx_sent, idx_recv).await.unwrap();
// Reveal the DNS name.
builder.server_identity();
// Finalize.
prover.finalize().await.unwrap()
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();
}
/// 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.
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 re = Regex::new(r#""homeworld"\s?:\s?"(.*?)""#).unwrap();
let homeworld_match = re.captures(&received_string).unwrap().get(1).unwrap();
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());
// Reveal everything except for the homeworld.
let start = homeworld_match.start();
let end = homeworld_match.end();
Idx::new([0..start, end..recv_transcript_len])
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(prover: &mut Prover<Prove>) -> Idx {
let sent_transcript = prover.transcript().sent();
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();
@@ -165,8 +183,9 @@ fn redact_and_reveal_sent_data(prover: &mut Prover<Prove>) -> Idx {
debug!("Send data: {}", sent_string);
// Reveal everything except for the SECRET.
Idx::new([
[
0..secret_start,
secret_start + SECRET.len()..sent_transcript_len,
])
]
.into()
}

View File

@@ -1 +1,8 @@
package-lock.json
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -1,165 +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';
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 />);
function App(): ReactElement {
const [processing, setProcessing] = useState(false);
const [result, setResult] = useState<string | null>(null);
const onClick = useCallback(async () => {
setProcessing(true);
const url = 'https://swapi.dev/api/people/1';
const method: Method = 'GET';
const headers = {
secret: "TLSNotary's private key",
'Content-Type': 'application/json',
};
const body = {};
// let websocketProxyUrl = 'wss://notary.pse.dev/proxy';
const websocketProxyUrl = 'ws://localhost:55688';
const verifierProxyUrl = 'ws://localhost:9816/verify';
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 {
console.time('reveal');
const reveal = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['connection'],
transcript.ranges.sent.headers!['host'],
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['content-length'],
...transcript.ranges.sent.lineBreaks,
],
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers['server'],
transcript.ranges.recv.headers['date'],
transcript.ranges.recv.headers['content-type'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['eye_color'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
],
};
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>
<h1>TLSNotary interactive prover demo</h1>
<div>
Before clicking the start button, make sure the{' '}
<i>interactive verifier</i> and the <i>web socket proxy</i> are running.
Check the <a href="README.md">README</a> for the details.
</div>
<br />
<button onClick={!processing ? onClick : undefined} disabled={processing}>
Start Prover
</button>
<br />
<div>
<b>Proof: </b>
{!processing && !result ? (
<i>not started yet</i>
) : !result ? (
<>
Proving data from swapi...
<Watch
visible={true}
height="40"
width="40"
radius="48"
color="#000000"
ariaLabel="watch-loading"
wrapperStyle={{}}
wrapperClass=""
/>
Open <i>Developer tools</i> to follow progress
</>
) : (
<>
<pre>{JSON.stringify(result, null, 2)}</pre>
</>
)}
</div>
</div>
);
}

View File

@@ -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>

View File

@@ -4,15 +4,21 @@
"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",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.1",
"css-loader": "^7.1.2",
"postcss-loader": "^8.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"tlsn-js": "file:../../.."
"style-loader": "^4.0.0",
"tlsn-js": "../../.."
},
"devDependencies": {
"@types/react": "^18.0.26",
@@ -20,11 +26,16 @@
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"html-webpack-plugin": "^5.5.0",
"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",
"tailwindcss": "^3.4.16",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}
}

View 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,
}
]
});

View File

@@ -0,0 +1,4 @@
const tailwindcss = require("tailwindcss");
module.exports = {
plugins: ["postcss-preset-env", tailwindcss],
};

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,268 @@
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 />);
const serverUrl = 'https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.12/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 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,
maxRecvData: 2000
})) 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.log("test", body.information.address.street);
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:', 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 GitHub...</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 data-testid="proof-data" 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,
};
}

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#243f5f',
},
},
},
plugins: [],
};

View 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 });
});

View File

@@ -20,7 +20,7 @@
"jsx": "react"
},
"include": [
"app.tsx",
"worker.ts"
"src/app.tsx",
"src/worker.ts"
]
}
}

View File

@@ -27,7 +27,7 @@ var options = {
],
mode: 'development',
entry: {
app: path.join(__dirname, 'app.tsx'),
app: path.join(__dirname, 'src', 'app.tsx'),
},
output: {
filename: '[name].bundle.js',
@@ -51,6 +51,9 @@ var options = {
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'source-map-loader',
},
{
loader: require.resolve('ts-loader'),
},
@@ -68,6 +71,29 @@ var options = {
],
exclude: /node_modules/,
},
{
// look for .css or .scss files
test: /\.(css|scss)$/,
// in the `web` directory
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: { importLoaders: 1 },
},
{
loader: 'postcss-loader',
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
},
],
},
resolve: {
@@ -75,6 +101,11 @@ var options = {
extensions: fileExtensions
.map((extension) => '.' + extension)
.concat(['.js', '.jsx', '.ts', '.tsx', '.css']),
fallback: {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
vm: require.resolve('vm-browserify'),
},
},
plugins: [
new CopyWebpackPlugin({
@@ -87,15 +118,16 @@ var options = {
],
}),
new CopyWebpackPlugin({
patterns: [
{ from: '../README.md', to: 'README.md' },
],
patterns: [{ from: '../README.md', to: 'README.md' }],
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'index.ejs'),
filename: 'index.html',
cache: false,
}),
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
@@ -105,13 +137,16 @@ var options = {
// - https://github.com/GoogleChromeLabs/wasm-bindgen-rayon#setting-up
// - https://web.dev/i18n/en/coop-coep/
devServer: {
port: 3456,
port: 8080,
host: 'localhost',
hot: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
client: {
overlay: false,
},
},
};

View File

@@ -17,22 +17,28 @@ hyper = { version = "1.1", features = ["client", "http1", "server"] }
hyper-util = { version = "0.1", features = ["full"] }
serde = { version = "1.0.147", features = ["derive"] }
sha1 = "0.10"
tokio = {version = "1", features = [
tokio = { version = "1", features = [
"rt",
"rt-multi-thread",
"macros",
"net",
"io-std",
"fs",
]}
] }
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"] }
ws_stream_tungstenite = { version = "0.13", features = ["tokio_io"] }
tlsn-core = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.7", package = "tlsn-core" }
tlsn-verifier = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.7", package = "tlsn-verifier" }
tlsn-common = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.7", package = "tlsn-common" }
tlsn-core = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.12", package = "tlsn-core" }
tlsn-verifier = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.12", package = "tlsn-verifier" }
tlsn-common = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.12", package = "tlsn-common" }
tower-util = "0.3.1"
# --- Transitive dependency pins (for TLSNotary alpha.12)---
aes = "=0.9.0-rc.0"
cipher = "=0.5.0-rc.0"
crypto-common = "=0.2.0-rc.3"
inout = "=0.2.0-rc.5"

View File

@@ -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

View File

@@ -13,7 +13,8 @@ use std::{
sync::Arc,
};
use tlsn_common::config::ProtocolConfigValidator;
use tlsn_verifier::{SessionInfo, Verifier, VerifierConfig};
use tlsn_core::{VerifierOutput, VerifyConfig};
use tlsn_verifier::{Verifier, VerifierConfig};
use tokio::{
io::{AsyncRead, AsyncWrite},
@@ -44,7 +45,7 @@ pub async fn run_server(
) -> 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}")
eyre!("Failed to parse verifier host address from server config: {err}")
})?),
verifier_port,
);
@@ -105,10 +106,10 @@ async fn handle_socket(socket: WebSocket, verifier_globals: VerifierGlobals) {
let stream = WsStream::new(socket.into_inner());
match verifier(stream, &verifier_globals.server_domain).await {
Ok((sent, received, _session_info)) => {
Ok((sent, received)) => {
info!("Successfully verified {}", &verifier_globals.server_domain);
info!("Verified sent data:\n{}", sent,);
println!("Verified received data:\n{}", received,);
println!("Verified received data:\n{received}",);
}
Err(err) => {
error!("Failed verification using websocket: {err}");
@@ -119,7 +120,7 @@ async fn handle_socket(socket: WebSocket, verifier_globals: VerifierGlobals) {
async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
socket: T,
server_domain: &str,
) -> Result<(String, String, SessionInfo), eyre::ErrReport> {
) -> Result<(String, String), eyre::ErrReport> {
debug!("Starting verification...");
// Setup Verifier.
@@ -135,15 +136,24 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
.unwrap();
let verifier = Verifier::new(verifier_config);
// Verify MPC-TLS and wait for (redacted) data.
// Receive authenticated 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);
let verify_config = VerifyConfig::default();
let VerifierOutput {
server_name,
transcript,
..
} = verifier
.verify(socket.compat(), &verify_config)
.await
.unwrap();
let transcript = transcript.expect("prover should have revealed transcript data");
// Check sent data: check host.
debug!("Starting sent data verification...");
let sent = partial_transcript.sent_unsafe().to_vec();
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)
@@ -151,21 +161,28 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
// Check received data: check json and version number.
debug!("Starting received data verification...");
let received = partial_transcript.received_unsafe().to_vec();
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("eye_color")
.ok_or_else(|| eyre!("Verification failed: missing eye_color in received data"))?;
.find("123 Elm Street")
.ok_or_else(|| eyre!("Verification failed: missing data 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"));
if let Some(server_name) = server_name {
if server_name.as_str() != server_domain {
return Err(eyre!("Verification failed: server name mismatches"));
}
} else {
// TODO: https://github.com/tlsnotary/tlsn-js/issues/110
// return Err(eyre!("Verification failed: server name is missing"));
}
let sent_string = bytes_to_redacted_string(&sent)?;
let received_string = bytes_to_redacted_string(&received)?;
Ok((sent_string, received_string, session_info))
Ok((sent_string, received_string))
}
/// Render redacted bytes as `🙈`.

View File

@@ -7,7 +7,7 @@ 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";
const SERVER_DOMAIN: &str = "raw.githubusercontent.com";
#[tokio::main]
async fn main() -> Result<(), eyre::ErrReport> {

View File

@@ -1 +1,8 @@
package-lock.json
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -0,0 +1,78 @@
# TLSNotary in React/TypeScript with `tlsn-js`
This demo shows how to use TLSNotary with a delegated verifier, also known as a **notary**.
In this demo, we request JSON data from a GitHub page, use `tlsn-js` to notarize the TLS request with TLSNotary, and display the attestation and revealed data.
> **Note:**
> This demo uses TLSNotary to notarize **public** data for simplicity. In real-world applications, TLSNotary is especially valuable for notarizing private and sensitive data.
---
## Setup
Before running the demo, you need to start a local notary server and a websocket proxy. If you prefer to use the hosted test servers from PSE, see the section below.
### Websocket Proxy
Browsers cannot make raw TCP connections, so a websocket proxy server is required.
1. **Install [wstcp](https://github.com/sile/wstcp):**
```sh
cargo install wstcp
```
2. **Run a websocket proxy for `https://raw.githubusercontent.com`:**
```sh
wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443
```
> Note: The `raw.githubusercontent.com:443` argument specifies the server used in this quick start.
### Run a Local Notary Server
You also need to run a local notary server for this demo.
- **Using Git and Rust Cargo:**
```sh
git clone https://github.com/tlsnotary/tlsn.git
cargo run --release --bin notary-server
```
- **Using Docker (from the root of the tlsn-js repo):**
```sh
npm run notary
```
The notary server will now be running in the background, waiting for connections.
---
### Use the PSE Web Proxy and Notary
If you want to use the hosted PSE notary and proxy:
1. Open `app.tsx` in your editor.
2. Replace the notary URL:
```ts
notaryUrl: 'https://notary.pse.dev/v0.1.0-alpha.11',
```
This uses the [PSE](https://pse.dev) notary server to notarize the API request. You can use a different or [local notary](#run-a-local-notary-server); a local server will be faster due to the high bandwidth and low network latency.
3. Replace the websocket proxy URL:
```ts
websocketProxyUrl: 'wss://notary.pse.dev/proxy?token=raw.githubusercontent.com',
```
This uses a proxy hosted by [PSE](https://pse.dev). You can use a different or local proxy if you prefer.
---
## Run the Demo
1. **Install dependencies:**
```sh
npm i
```
2. **Start the Webpack Dev Server:**
```sh
npm run dev
```
3. **Open the demo in your browser:**
Go to [http://localhost:8080](http://localhost:8080)
4. **Click the "Start demo" button**
5. **Open Developer Tools** and monitor the console logs

View File

@@ -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>

View File

@@ -5,24 +5,37 @@
"main": "webpack.js",
"scripts": {
"dev": "webpack-dev-server --config webpack.js",
"build": "webpack --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",
"css-loader": "^7.1.2",
"http-parser-js": "^0.5.9",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"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": "../../"
},
"devDependencies": {
"@playwright/test": "^1.52.0",
"@types/node": "^22.15.18",
"@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",
"source-map-loader": "^5.0.0",
"stream-browserify": "^3.0.0",
"ts-loader": "^9.4.2",
@@ -32,4 +45,4 @@
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}
}

View 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: './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,
}
]
});

View File

@@ -0,0 +1,4 @@
const tailwindcss = require("tailwindcss");
module.exports = {
plugins: ["postcss-preset-env", tailwindcss],
};

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -8,9 +8,12 @@ import {
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)),
);
@@ -20,6 +23,18 @@ 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.12';
const websocketProxyUrl = local
? 'ws://localhost:55688'
: 'wss://notary.pse.dev/proxy?token=raw.githubusercontent.com';
const loggingLevel = 'Info'; // https://github.com/tlsnotary/tlsn/blob/main/crates/wasm/src/log.rs#L8
const serverUrl = 'https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.12/crates/server-fixture/server/src/data/1kb.json';
const serverDns = 'raw.githubusercontent.com';
function App(): ReactElement {
const [initialized, setInitialized] = useState(false);
const [processing, setProcessing] = useState(false);
@@ -29,25 +44,28 @@ function App(): ReactElement {
useEffect(() => {
(async () => {
await init({ loggingLevel: 'Info' });
await init({ loggingLevel: loggingLevel });
setInitialized(true);
})();
}, []);
const onClick = useCallback(async () => {
setProcessing(true);
const notary = NotaryServer.from(`http://localhost:7047`);
const notary = NotaryServer.from(notaryUrl);
console.time('submit');
const prover = (await new Prover({
serverDns: 'swapi.dev',
serverDns: serverDns,
maxRecvData: 2048,
})) as TProver;
await prover.setup(await notary.sessionUrl());
const resp = await prover.sendRequest('ws://localhost:55688', {
url: 'https://swapi.dev/api/people/1',
const resp = await prover.sendRequest(websocketProxyUrl, {
url: serverUrl,
method: 'GET',
headers: {
'Content-Type': 'application/json',
secret: 'test_secret',
},
body: {
hello: 'world',
@@ -60,23 +78,44 @@ function App(): ReactElement {
console.time('transcript');
const transcript = await prover.transcript();
console.log(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: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['host'],
...transcript.ranges.sent.lineBreaks,
],
sent: subtractRanges(
{ start: 0, end: sent.length },
mapStringToRange(
['secret: test_secret'],
Buffer.from(sent).toString('utf-8'),
),
),
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers!['server'],
transcript.ranges.recv.headers!['date'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
...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'),
),
],
};
const notarizationOutputs = await prover.notarize(commit);
@@ -88,9 +127,10 @@ function App(): ReactElement {
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: commit,
reveal: { ...commit, server_identity: false },
})) as TPresentation;
console.log(await presentation.serialize());
setPresentationJSON(await presentation.json());
console.timeEnd('proof');
}, [setPresentationJSON, setProcessing]);
@@ -98,13 +138,15 @@ function App(): ReactElement {
const onAltClick = useCallback(async () => {
setProcessing(true);
const proof = await (Prover.notarize as typeof TProver.notarize)({
notaryUrl: 'http://localhost:7047',
websocketProxyUrl: 'ws://localhost:55688',
url: 'https://swapi.dev/api/people/1',
notaryUrl: notaryUrl,
websocketProxyUrl: websocketProxyUrl,
maxRecvData: 2048,
url: serverUrl,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
body: {
hello: 'world',
one: 1,
@@ -124,12 +166,12 @@ function App(): ReactElement {
const proof = (await new Presentation(
presentationJSON.data,
)) as TPresentation;
const notary = NotaryServer.from(`http://localhost:7047`);
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,
sent: verifierOutput.transcript?.sent || [],
recv: verifierOutput.transcript?.recv || [],
});
const vk = await proof.verifyingKey();
setResult({
@@ -146,61 +188,161 @@ function App(): ReactElement {
}, [presentationJSON, setResult]);
return (
<div>
<div>
<button
onClick={!processing ? onClick : undefined}
disabled={processing || !initialized}
>
Start Demo (Normal config)
</button>
<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://tlsnotary.org/docs/quick_start/tlsn-js/"
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>
<button
onClick={!processing ? onAltClick : undefined}
disabled={processing || !initialized}
>
Start Demo 2 (With helper method)
</button>
<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>
<div>
<b>Proof: </b>
{!processing && !presentationJSON ? (
<i>not started</i>
) : !presentationJSON ? (
<>
Proving data from swapi...
<Watch
visible={true}
height="40"
width="40"
radius="48"
color="#000000"
ariaLabel="watch-loading"
wrapperStyle={{}}
wrapperClass=""
/>
Open <i>Developer tools</i> to follow progress
</>
) : (
<>
<details>
<summary>View Proof</summary>
<pre>{JSON.stringify(presentationJSON, null, 2)}</pre>
{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 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 data-testid="proof-data"
className="mt-2 p-2 bg-slate-100 rounded text-sm text-slate-800 overflow-auto"
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}
>
{JSON.stringify(presentationJSON, null, 2)}
</pre>
</details>
</>
)}
</div>
<div>
<b>Verification: </b>
{!presentationJSON ? (
<i>not started</i>
) : !result ? (
<i>verifying</i>
) : (
<pre>{JSON.stringify(result, null, 2)}</pre>
)}
)}
</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 data-testid="verify-data"
className="mt-2 p-2 bg-slate-100 rounded text-sm text-slate-800 overflow-auto"
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}
>
{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,
};
}

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#243f5f',
},
},
},
plugins: [],
};

View File

@@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/TLSNotary React TypeScript Demo/)
});
test('run demo (normal)', async ({ page }) => {
test.setTimeout(60000);
await page.goto('/');
// Click the get started link.
await page.getByRole('button', { name: 'Start Demo (Normal config)' }).click();
await expect(page.getByTestId('proof-data')).toContainText('"data":', { timeout: 60000 });
let verify_data = await page.getByTestId('verify-data').innerText();
expect(verify_data).toContain('"serverName": "raw.githubusercontent.com"');
expect(verify_data).toContain('John Doe');
});
test('run demo (helper)', async ({ page }) => {
test.setTimeout(60000);
await page.goto('/');
// Click the get started link.
await page.getByRole('button', { name: 'Start Demo 2 (With helper method)' }).click();
await expect(page.getByTestId('proof-data')).toContainText('"data":', { timeout: 60000 });
// await page.screenshot({ path: 'screenshot.png', fullPage: true });
let verify_data = await page.getByTestId('verify-data').innerText();
expect(verify_data).toContain('"serverName": "raw.githubusercontent.com"');
expect(verify_data).toContain('"recv"');
});

View File

@@ -71,6 +71,29 @@ var options = {
],
exclude: /node_modules/,
},
{
// look for .css or .scss files
test: /\.(css|scss)$/,
// in the `web` directory
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: { importLoaders: 1 },
},
{
loader: 'postcss-loader',
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
},
],
},
resolve: {
@@ -111,10 +134,16 @@ var options = {
// - https://github.com/GoogleChromeLabs/wasm-bindgen-rayon#setting-up
// - https://web.dev/i18n/en/coop-coep/
devServer: {
port: 8080,
host: 'localhost',
hot: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
client: {
overlay: false,
},
},
};

View File

@@ -1 +1,8 @@
package-lock.json
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -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.

View File

@@ -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>

View File

@@ -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",
@@ -20,7 +22,7 @@
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"tailwindcss": "^3.4.14",
"tlsn-js": "0.1.0-alpha.7.1",
"tlsn-js": "../../",
"ws": "^8.18.0"
},
"devDependencies": {
@@ -45,4 +47,4 @@
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}
}

View 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,
}
]
});

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,12 @@ import {
Verifier as TVerifier,
Commit,
Transcript,
subtractRanges,
mapStringToRange,
} from 'tlsn-js';
import './app.scss';
import WebSocketStream from './stream';
import { HTTPParser } from 'http-parser-js';
const { init, Prover, Verifier }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
@@ -24,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.12/crates/server-fixture/server/src/data/1kb.json`;
function App(): ReactElement {
const [ready, setReady] = useState(false);
@@ -37,7 +40,7 @@ function App(): ReactElement {
// Initialize TLSNotary
useEffect(() => {
(async () => {
await init({ loggingLevel: 'Debug' });
await init({ loggingLevel: 'Info' });
setReady(true);
})();
}, []);
@@ -93,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');
@@ -152,37 +158,57 @@ function App(): ReactElement {
addProverLog('Response received');
addProverLog('Transcript sent');
addProverLog(transcript.sent);
addProverLog(Buffer.from(transcript.sent).toString('utf-8'));
addProverLog('Transcript received');
addProverLog(transcript.recv);
addProverLog(Buffer.from(transcript.recv).toString('utf-8'));
addProverLog('Revealing data to verifier');
const { sent, recv } = transcript;
const {
info: recvInfo,
headers: recvHeaders,
body: recvBody,
} = parseHttpMessage(Buffer.from(recv), 'response');
const body = JSON.parse(recvBody[0].toString());
// Prover only reveals parts the transcript to the verifier
const commit: Commit = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['host'],
...transcript.ranges.sent.lineBreaks,
],
sent: subtractRanges(
{ start: 0, end: sent.length },
mapStringToRange(
['secret: test_secret'],
Buffer.from(sent).toString('utf-8'),
),
),
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers!['server'],
transcript.ranges.recv.headers!['date'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
...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'),
),
],
};
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:');
@@ -192,49 +218,62 @@ function App(): ReactElement {
}, [ready]);
return (
<div className="w-screen h-screen flex flex-col overflow-hidden">
<div className="w-full p-2.5 bg-slate-200 mb-5 flex-shrink-0">
<h1>Web-to-Web P2P Demo</h1>
<p>
<div className="w-screen h-screen flex flex-col bg-slate-100 overflow-hidden">
<div className="w-full p-4 bg-slate-800 text-white flex-shrink-0 shadow-md">
<h1 className="text-xl font-bold">Web-to-Web P2P Demo</h1>
<p className="text-sm mt-1">
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" target="_blank" rel="noopener noreferrer">
swapi.dev
<a
href="https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.12/crates/server-fixture/server/src/data/1kb.json"
target="_blank"
rel="noopener noreferrer"
className="underline text-blue-400 hover:text-blue-300"
>
our GitHub repository
</a>{' '}
and proves it to the verifier.
</p>
</div>
<div className="grid grid-rows-2 grid-cols-2 p-2 gap-2 flex-grow">
<div className="flex flex-col items-center border border-slate-300 bg-slate-50 rounded row-span-1 col-span-1 p-4 gap-2">
<div className="font-semibold">Prover</div>
<div className="flex flex-col text-sm bg-white border border-slate-300 w-full flex-grow cursor-text py-1 overflow-y-auto">
<div className="grid grid-rows-2 grid-cols-2 gap-4 p-4 flex-grow">
<div className="flex flex-col items-center border border-slate-300 bg-white rounded-lg shadow-md row-span-1 col-span-1 p-4 gap-2">
<div className="font-semibold text-slate-700 text-lg">Prover</div>
<div className="flex flex-col text-sm bg-slate-50 border border-slate-200 w-full flex-grow py-2 overflow-y-auto rounded">
{proverMessages.map((m, index) => (
<span key={index} className="px-2 py-1 text-slate-600 break-all">
{m}
</span>
))}
</div>
</div>
<div className="flex flex-col items-center border border-slate-300 bg-slate-100 rounded row-span-1 col-span-1 p-4 gap-2">
<div className="font-semibold">Verifier</div>
<div className="flex flex-col text-sm bg-white border border-slate-300 w-full flex-grow cursor-text py-1 overflow-y-auto">
{verifierMessages.map((m, index) => (
<span
key={index}
className="px-1 py-0.5 text-slate-600 break-all"
data-testid="prover-data"
className="px-3 py-1 text-slate-600 break-all"
>
{m}
</span>
))}
</div>
</div>
<div className="flex flex-row justify-center row-span-1 col-span-2">
<div className="flex flex-col items-center border border-slate-300 bg-white rounded-lg shadow-md row-span-1 col-span-1 p-4 gap-2">
<div className="font-semibold text-slate-700 text-lg">Verifier</div>
<div className="flex flex-col text-sm bg-slate-50 border border-slate-200 w-full flex-grow py-2 overflow-y-auto rounded">
{verifierMessages.map((m, index) => (
<span
key={index}
data-testid="verifier-data"
className="px-3 py-1 text-slate-600 break-all"
>
{m}
</span>
))}
</div>
</div>
<div className="flex flex-row justify-center items-center row-span-1 col-span-2">
<Button
className="h-fit"
className="bg-slate-800 text-white font-semibold px-6 py-3 rounded-lg shadow-md hover:bg-slate-700 disabled:opacity-50"
disabled={!ready || started}
onClick={start}
>
<div>
<div data-testid="start" className="flex items-center">
{ready && !started ? (
<>Start Demo</>
) : (
@@ -243,10 +282,8 @@ function App(): ReactElement {
height="40"
width="40"
radius="48"
color="#000000"
color="#ffffff"
ariaLabel="watch-loading"
wrapperStyle={{}}
wrapperClass=""
/>
)}
</div>
@@ -270,3 +307,35 @@ function Button(props: any) {
/>
);
}
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,
};
}

View 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);
});

View File

@@ -134,7 +134,7 @@ var options = {
// - https://github.com/GoogleChromeLabs/wasm-bindgen-rayon#setting-up
// - https://web.dev/i18n/en/coop-coep/
devServer: {
port: 3456,
port: 8080,
host: 'localhost',
hot: true,
headers: {

18940
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-js",
"version": "0.1.0-alpha.7.1",
"version": "0.1.0-alpha.12.0",
"description": "",
"repository": "https://github.com/tlsnotary/tlsn-js",
"main": "build/lib.js",
@@ -15,46 +15,44 @@
"serve:test": "serve --config ../serve.json ./test-build -l 3001",
"build:src": "webpack --config webpack.build.config.js",
"build:types": "tsc --project tsconfig.compile.json",
"build:tlsn-binaries": "sh utils/build-tlsn-binaries.sh v0.1.0-alpha.7",
"build:lib": "NODE_ENV=production concurrently npm:build:src npm:build:types",
"build:wasm": "sh tlsn-wasm/build.sh v0.1.0-alpha.12",
"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:test": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha -r ts-node/register 'test/testRunner.ts'",
"test": "npm run build:tlsn-binaries && npm run build:test && npm run run:test",
"test:only": "npm run build:test && npm run run:test"
"test": "playwright test",
"notary": "docker run --platform=linux/amd64 -p 7047:7047 --rm ghcr.io/tlsnotary/tlsn/notary-server:v0.1.0-alpha.12"
},
"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",
"comlink": "^4.4.1",
"comlink": "4.4.1",
"concurrently": "^5.1.0",
"constants-browserify": "^1.0.0",
"copy-webpack-plugin": "^5.0.5",
"copy-webpack-plugin": "^11.0.0",
"crypto-browserify": "^3.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"file-loader": "^5.0.2",
"html-webpack-plugin": "~5.3.2",
"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",
@@ -69,6 +67,6 @@
"node": ">= 16.20.2"
},
"dependencies": {
"tlsn-wasm": "^0.1.0-alpha.7.2"
"tlsn-wasm": "0.1.0-alpha.12"
}
}

View 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.12');
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"');
});

View 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
View 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,
},
]
});

9292
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.7"
```
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

203
readme.md
View File

@@ -1,158 +1,131 @@
![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.
## Example
```ts
// worker.ts
import * as Comlink from 'comlink';
import init, { Prover, NotarizedSession, TlsProof } from 'tlsn-js';
## Examples
Comlink.expose({
init,
Prover,
NotarizedSession,
TlsProof,
});
`tlsn-js` can be used in several modes depending on your use case.
```
```ts
// app.ts
import { NotaryServer } from 'tlsn-js';
const { init, Prover, NotarizedSession, TlsProof }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
The `./demo` folder contains three demos:
// To create a proof
await init({ loggingLevel: 'Debug '});
const notary = NotaryServer.from(`http://localhost:7047`);
const prover = await new Prover({ serverDns: 'swapi.dev' });
- `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.
// Connect to verifier
await prover.setup(await notary.sessionUrl());
## Running a Local WebSocket Proxy
// Submit request
await prover.sendRequest('ws://localhost:55688', {
url: 'https://swapi.dev/api/people/1',
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
body: {
hello: 'world',
one: 1,
},
});
In the demos, we attest data from `https://raw.githubusercontent.com`. Since browsers do not support raw TCP connections, a WebSocket proxy is required:
// Get transcript and precalculated ranges
const transcript = await prover.transcript();
1. Install [wstcp](https://github.com/sile/wstcp):
// Select ranges to commit
const commit: Commit = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['host'],
...transcript.ranges.sent.lineBreaks,
],
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers!['server'],
transcript.ranges.recv.headers!['date'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
],
};
| Tool | Command |
| ------ | ----------------------------- |
| cargo | `cargo install wstcp` |
| brew | `brew install wstcp` |
| source | https://github.com/sile/wstcp |
// Notarize selected ranges
const serializedSession = await prover.notarize(commit);
2. Run a WebSocket proxy for `https://raw.githubusercontent.com`:
// Instantiate NotarizedSession
// note: this is necessary because workers can only post messages in serializable values
const notarizedSession = await new NotarizedSession(serializedSession);
// Create proof for commited ranges
// note: this will reveal the selected ranges
const serializedProof = await notarizedSession.proof(commit);
// Instantiate Proof
// note: necessary due to limitation with workers
const proof = await new TlsProof(serializedProof);
// Verify a proof
const proofData = await proof.verify({
typ: 'P256',
key: await notary.publicKey(),
});
```
## Running a local websocket proxy for `https://swapi.dev`
1. Install [websocat](https://github.com/vi/websocat):
| tool | command |
| ------ | ------------------------------ |
| cargo | `cargo install websocat` |
| brew | `brew install websocat` |
| source | https://github.com/vi/websocat |
2. Run a websocket proxy for `https://swapi.dev`:
```sh
websocat --binary -v ws-l:0.0.0.0:55688 tcp:swapi.dev:443
```
```sh
wstcp --bind-addr 127.0.0.1:55688 raw.githubusercontent.com:443
```
## Install as NPM Package
```
```sh
npm install tlsn-js
```
## Development
```
# make sure you have rust installed
# https://www.rust-lang.org/tools/install
npm install
This library wraps the `tlsn-wasm` module.
# this serve a page that will execute the example code at http://localhost:3001
npm run dev
To work on both `tlsn-wasm` and `tlsn-js` locally, update `package.json`:
```json
"tlsn-wasm": "./tlsn-wasm/pkg"
```
Then build `tlsn-wasm`:
```sh
npm run build:wasm
```
Next:
```sh
npm install
npm run test
```
> 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
```
```sh
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 fin `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
```

View File

@@ -1,6 +1,5 @@
import initWasm, {
initThreadPool,
init_logging,
initialize,
LoggingLevel,
LoggingConfig,
Attestation as WasmAttestation,
@@ -11,6 +10,7 @@ import initWasm, {
Prover as WasmProver,
type ProverConfig,
type Method,
NetworkSetting,
VerifierConfig,
VerifierOutput,
VerifyingKey,
@@ -19,18 +19,14 @@ import initWasm, {
ConnectionInfo,
PartialTranscript,
} from 'tlsn-wasm';
import {
arrayToHex,
processTranscript,
expect,
headerToMap,
hexToArray,
} from './utils';
import { ParsedTranscriptData, PresentationJSON } from './types';
import { arrayToHex, expect, headerToMap, hexToArray } from './utils';
import { PresentationJSON, } from './types';
import { Buffer } from 'buffer';
import { Transcript, subtractRanges, mapStringToRange } from './transcript';
let LOGGING_LEVEL: LoggingLevel = 'Info';
function debug(...args: any[]) {
function debug(...args: unknown[]) {
if (['Debug', 'Trace'].includes(LOGGING_LEVEL)) {
console.log('tlsn-js DEBUG', ...args);
}
@@ -49,18 +45,20 @@ export default async function init(config?: {
const res = await initWasm();
init_logging({
level: loggingLevel,
crate_filters: undefined,
span_events: undefined,
});
// 6422528 ~= 6.12 mb
debug('res.memory', res.memory);
debug('res.memory.buffer.length', res.memory.buffer.byteLength);
debug('initialize thread pool');
await initThreadPool(hardwareConcurrency);
await initialize(
{
level: loggingLevel,
crate_filters: undefined,
span_events: undefined,
},
hardwareConcurrency,
);
debug('initialized thread pool');
}
@@ -78,12 +76,17 @@ export class Prover {
headers?: {
[name: string]: string;
};
body?: any;
body?: unknown;
maxSentData?: number;
maxSentRecords?: number,
maxRecvData?: number;
maxRecvDataOnline?: number;
maxRecvRecordsOnline?: number,
network?: NetworkSetting
deferDecryptionFromStart?: boolean;
commit?: Commit;
serverIdentity?: boolean
clientAuth?: [number[][], number[]];
}): Promise<PresentationJSON> {
const {
url,
@@ -91,21 +94,30 @@ export class Prover {
headers = {},
body,
maxSentData = 1024,
maxSentRecords,
maxRecvData = 1024,
maxRecvDataOnline,
maxRecvRecordsOnline,
network = 'Bandwidth',
deferDecryptionFromStart,
notaryUrl,
websocketProxyUrl,
commit: _commit,
serverIdentity = false,
clientAuth,
} = options;
const hostname = new URL(url).hostname;
const notary = NotaryServer.from(notaryUrl);
const prover = new WasmProver({
server_name: hostname,
max_sent_data: maxSentData,
max_sent_records: maxSentRecords,
max_recv_data: maxRecvData,
max_recv_data_online: maxRecvDataOnline,
max_recv_records_online: maxRecvRecordsOnline,
defer_decryption_from_start: deferDecryptionFromStart,
network: network,
client_auth: clientAuth,
});
await prover.setup(await notary.sessionUrl(maxSentData, maxRecvData));
@@ -128,13 +140,14 @@ export class Prover {
const { attestation, secrets } = await prover.notarize(commit);
const presentation = build_presentation(attestation, secrets, commit);
const reveal: Reveal = { ...commit, server_identity: serverIdentity }
const presentation = build_presentation(attestation, secrets, reveal);
return {
version: '0.1.0-alpha.7',
version: '0.1.0-alpha.12',
data: arrayToHex(presentation.serialize()),
meta: {
notaryUrl: notaryUrl,
notaryUrl: notary.normalizeUrl(),
websocketProxyUrl: websocketProxyUrl,
},
};
@@ -143,16 +156,24 @@ export class Prover {
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);
}
@@ -166,27 +187,14 @@ export class Prover {
return this.#prover.setup(verifierUrl);
}
async transcript(): Promise<{
sent: string;
recv: string;
ranges: { recv: ParsedTranscriptData; sent: ParsedTranscriptData };
}> {
async transcript(): Promise<{ sent: number[]; recv: number[] }> {
const transcript = this.#prover.transcript();
const recv = Buffer.from(transcript.recv).toString();
const sent = Buffer.from(transcript.sent).toString();
return {
recv,
sent,
ranges: {
recv: processTranscript(recv),
sent: processTranscript(sent),
},
};
return { sent: transcript.sent, recv: transcript.recv };
}
static getHeaderMap(
url: string,
body?: any,
body?: unknown,
headers?: { [key: string]: string },
) {
const hostname = new URL(url).hostname;
@@ -217,7 +225,7 @@ export class Prover {
url: string;
method?: Method;
headers?: { [key: string]: string };
body?: any;
body?: unknown;
},
): Promise<{
status: number;
@@ -273,17 +281,18 @@ export class Prover {
async reveal(reveal: Reveal) {
return this.#prover.reveal(reveal);
}
}
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);
}
@@ -305,12 +314,12 @@ export class Presentation {
constructor(
params:
| {
attestationHex: string;
secretsHex: string;
notaryUrl?: string;
websocketProxyUrl?: string;
reveal?: Reveal;
}
attestationHex: string;
secretsHex: string;
notaryUrl?: string;
websocketProxyUrl?: string;
reveal?: Reveal;
}
| string,
) {
if (typeof params === 'string') {
@@ -327,6 +336,7 @@ export class Presentation {
params.reveal || {
sent: [{ start: 0, end: transcript.sent.length }],
recv: [{ start: 0, end: transcript.recv.length }],
server_identity: false,
},
);
this.#websocketProxyUrl = params.websocketProxyUrl;
@@ -348,10 +358,12 @@ export class Presentation {
async json(): Promise<PresentationJSON> {
return {
version: '0.1.0-alpha.7',
version: '0.1.0-alpha.12',
data: await this.serialize(),
meta: {
notaryUrl: this.#notaryUrl,
notaryUrl: this.#notaryUrl
? NotaryServer.from(this.#notaryUrl).normalizeUrl()
: '',
websocketProxyUrl: this.#websocketProxyUrl,
},
};
@@ -455,6 +467,19 @@ export class NotaryServer {
.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,
@@ -482,48 +507,11 @@ export class NotaryServer {
}
}
export class Transcript {
#sent: number[];
#recv: number[];
constructor(params: { sent: number[]; recv: number[] }) {
this.#recv = params.recv;
this.#sent = params.sent;
}
static processRanges(text: string) {
return processTranscript(text);
}
recv(redactedSymbol = '*') {
return this.#recv.reduce((recv: string, num) => {
recv =
recv + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
return recv;
}, '');
}
sent(redactedSymbol = '*') {
return this.#sent.reduce((sent: string, num) => {
sent =
sent + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
return sent;
}, '');
}
text = (redactedSymbol = '*') => {
return {
sent: this.sent(redactedSymbol),
recv: this.recv(redactedSymbol),
};
};
}
export {
type ParsedTranscriptData,
type LoggingLevel,
type LoggingConfig,
type Commit,
type Method,
type Reveal,
type ProverConfig,
type VerifierConfig,
@@ -531,4 +519,7 @@ export {
type VerifierOutput,
type ConnectionInfo,
type PartialTranscript,
Transcript,
mapStringToRange,
subtractRanges,
};

107
src/transcript.ts Normal file
View File

@@ -0,0 +1,107 @@
import { Buffer } from 'buffer';
export class Transcript {
#sent: number[];
#recv: number[];
constructor(params: { sent: number[]; recv: number[] }) {
this.#recv = params.recv;
this.#sent = params.sent;
}
get raw() {
return {
recv: this.#recv,
sent: this.#sent,
};
}
recv(redactedSymbol = '*') {
return bytesToUtf8(substituteRedactions(this.#recv, redactedSymbol));
}
sent(redactedSymbol = '*') {
return bytesToUtf8(substituteRedactions(this.#sent, redactedSymbol));
}
text = (redactedSymbol = '*') => {
return {
sent: this.sent(redactedSymbol),
recv: this.recv(redactedSymbol),
};
};
}
export function subtractRanges(
ranges: { start: number; end: number },
negatives: { start: number; end: number }[],
): { start: number; end: number }[] {
const returnVal: { start: number; end: number }[] = [ranges];
negatives
.sort((a, b) => (a.start < b.start ? -1 : 1))
.forEach(({ start, end }) => {
const last = returnVal.pop()!;
if (start < last.start || end > last.end) {
console.error('invalid ranges');
return;
}
if (start === last.start && end === last.end) {
return;
}
if (start === last.start && end < last.end) {
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end < last.end) {
returnVal.push({ start: last.start, end: start });
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end === last.end) {
returnVal.push({ start: last.start, end: start });
return;
}
});
return returnVal;
}
export function mapStringToRange(secrets: string[], text: string) {
return secrets
.map((secret: string) => {
const byteIdx = indexOfString(text, secret);
return byteIdx > -1
? {
start: byteIdx,
end: byteIdx + bytesSize(secret),
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[];
}
function indexOfString(str: string, substr: string): number {
return Buffer.from(str).indexOf(Buffer.from(substr));
}
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));
}

View File

@@ -3,17 +3,8 @@ export type CommitData = {
end: number;
};
export type ParsedTranscriptData = {
all: CommitData;
info: CommitData;
headers: { [key: string]: CommitData };
body?: CommitData;
json?: { [path: string]: CommitData };
lineBreaks: CommitData[];
};
export type PresentationJSON = {
version: '0.1.0-alpha.7';
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';
data: string;
meta: {
notaryUrl?: string;

View File

@@ -1,401 +1,5 @@
import { ParsedTranscriptData } from './types';
import { Buffer } from 'buffer';
type Stack =
| {
type: 'object';
symbol: string;
range: [number, number];
id: number;
}
| {
type: 'array';
symbol: string;
range: [number, number];
data: string;
id: number;
}
| {
type: 'object_key';
symbol: string;
range: [number, number];
data: string;
path: string;
id: number;
objectId: number;
}
| {
type: 'object_value';
symbol: string;
range: [number, number];
data: string;
id: number;
keyId: number;
objectId: number;
}
| {
type: 'object_value_string';
symbol: string;
range: [number, number];
data: string;
path: string;
id: number;
objectId: number;
valueId: number;
}
| {
type: 'object_value_number';
symbol: string;
range: [number, number];
data: string;
path: string;
id: number;
objectId: number;
valueId: number;
};
type Commitment = {
name?: string;
path?: string;
start: number;
end: number;
};
export function processJSON(str: string): Commitment[] {
const json = JSON.parse(str);
expect(typeof json === 'object', 'json string must be an object');
const stack: Stack[] = [];
const values: Stack[] = [];
const keys: string[] = [];
let nonce = 0,
keyId = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charAt(i);
let last = stack[stack.length - 1];
if (last?.type === 'object_key') {
if (char === '"') {
last.range[1] = i;
const key = stack.pop();
expect(key!.type === 'object_key');
if (key?.type === 'object_key') {
keys.push(key.data);
key.path = keys.join('.');
values.push(key);
keyId = last.id;
}
} else {
last.data = last.data + char;
}
continue;
}
if (last?.type === 'object_value_string') {
if (char === '"') {
last.range[1] = i;
values.push(stack.pop()!);
const objectValue = stack.pop();
expect(
objectValue?.type === 'object_value',
'expect stack to be object_value',
);
objectValue!.range[1] = i;
values.push(objectValue!);
keys.pop();
} else {
last.data = last.data + char;
}
continue;
}
if (last?.type === 'array') {
if (char === ']') {
last.range[1] = i;
values.push(stack.pop()!);
} else if (char === '[') {
stack.push({
symbol: '[',
type: 'array',
id: nonce++,
range: [i, -1],
data: '',
});
} else {
last.data = last.data + char;
}
continue;
}
if (last?.type === 'object_value_number') {
if (char === ',' || char === '}') {
last.range[1] = i - 1;
values.push(stack.pop()!);
const objectValue = stack.pop();
expect(
objectValue?.type === 'object_value',
'expect stack to be object_value',
);
objectValue!.range[1] = i - 1;
values.push(objectValue!);
last = stack[stack.length - 1];
} else {
last.data = last.data + char;
continue;
}
}
if (last?.type === 'object_value') {
if (char === '}') {
last.range[1] = i - 1;
values.push(stack.pop()!);
const object = stack.pop();
expect(object?.type === 'object_value', 'expect stack to be object');
object!.range[1] = i;
values.push(object!);
keys.pop();
} else if (char === ',') {
last.range[1] = i - 1;
values.push(stack.pop()!);
keys.pop();
} else if (char === '{') {
stack.push({
symbol: '{',
type: 'object',
id: nonce++,
range: [i, -1],
});
} else if (char === '[') {
stack.push({
symbol: '[',
type: 'array',
id: nonce++,
range: [i, -1],
data: '',
});
} else if (char === '"') {
stack.push({
symbol: '"',
type: 'object_value_string',
objectId: last.objectId,
valueId: last.id,
id: nonce++,
data: '',
range: [i, -1],
path: '',
});
} else if (/^\d$/.test(char)) {
stack.push({
symbol: '"',
type: 'object_value_number',
objectId: last.objectId,
valueId: last.id,
id: nonce++,
data: '',
range: [i, -1],
path: '',
});
}
continue;
}
if (last?.type === 'object') {
switch (char) {
case '}':
last.range[1] = i;
values.push(stack.pop()!);
continue;
case '"':
stack.push({
symbol: '"',
type: 'object_key',
objectId: last.id,
id: nonce++,
data: '',
range: [i, -1],
path: '',
});
continue;
case ':':
stack.push({
symbol: ':',
type: 'object_value',
objectId: last.id,
keyId: keyId,
id: nonce++,
range: [i, -1],
data: '',
});
continue;
default:
continue;
}
}
switch (char) {
case '{':
stack.push({
symbol: '{',
type: 'object',
id: nonce++,
range: [i, -1],
});
break;
case '[':
stack.push({
symbol: '[',
type: 'array',
id: nonce++,
range: [i, -1],
data: '',
});
break;
}
}
expect(!stack.length, 'invalid stack length');
const commitments: {
[key: string]: Commitment;
} = {};
for (const value of values) {
if (value.type === 'object_key') {
commitments[value.id] = {
...(commitments[value.id] || {}),
path: value.path,
start: value.range[0],
};
} else if (value.type === 'object_value') {
commitments[value.keyId] = {
...(commitments[value.keyId] || {}),
end: value.range[1] + 1,
};
} else if (value.type === 'object') {
commitments[value.id] = {
start: value.range[0],
end: value.range[1] + 1,
};
} else if (value.type === 'array') {
commitments[value.id] = {
start: value.range[0],
end: value.range[1] + 1,
};
}
}
return Object.values(commitments).map(({ path, start, end }) => ({
path,
start,
end,
}));
}
export function processTranscript(transcript: string): ParsedTranscriptData {
// const commitments: Commitment[] = [];
const returnVal: ParsedTranscriptData = {
all: {
start: 0,
end: transcript.length,
},
info: {
start: 0,
end: transcript.indexOf('\n') + 1,
},
headers: {},
lineBreaks: [],
};
let text = '',
ptr = -1,
lineIndex = 0,
isBody = false;
for (let i = 0; i < transcript.length; i++) {
const char = transcript.charAt(i);
if (char === '\r') {
_processEOL(text, i, lineIndex++);
returnVal.lineBreaks.push({
start: i,
end: i + 1,
});
continue;
}
if (char === '\n') {
text = '';
ptr = -1;
returnVal.lineBreaks.push({
start: i,
end: i + 1,
});
continue;
}
if (ptr === -1) {
ptr = i;
}
text = text + char;
}
_processEOL(text, transcript.length - 1, lineIndex++);
return returnVal;
function _processEOL(txt: string, index: number, lineIndex: number) {
try {
if (!txt) return;
if (!isNaN(Number(txt))) {
isBody = true;
return;
}
const json = JSON.parse(txt);
returnVal.body = {
start: ptr,
end: index,
};
if (typeof json === 'object') {
const jsonCommits = processJSON(txt);
jsonCommits.forEach((commit) => {
if (commit.path) {
returnVal.json = returnVal.json || {};
returnVal.json[commit.path] = {
start: commit.start + ptr,
end: commit.end + ptr,
};
}
});
}
} catch (e) {
const [name, value] = txt.split(': ');
if (lineIndex === 0) {
returnVal.info = {
start: ptr,
end: index,
};
} else if (!isBody && value) {
returnVal.headers = returnVal.headers || {};
returnVal.headers[name.toLowerCase()] = {
start: ptr,
end: index,
};
} else if (isBody) {
returnVal.body = {
// value: txt,
start: ptr,
end: index,
};
}
}
}
}
export function expect(cond: any, msg = 'invalid expression') {
if (!cond) throw new Error(msg);
}

View File

@@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBv36FI4ZFszJa0DQFJ3wWCXvVLFr
cRzMG5kaTeHGoSzDu6cFqx3uEWYpFGo6C0EOUgf+mEgbktLrXocv5yHzKg==
-----END PUBLIC KEY-----

View File

@@ -1,7 +0,0 @@
{
"serverName": "example.com",
"time": 1708595467,
"sent": "GET / HTTP/1.1\r\nhost: example.com\r\naccept: */*\r\naccept-encoding: identity\r\nconnection: close\r\nuser-agent: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\r\n\r\n",
"recv": "HTTP/1.1 200 OK\r\nAge: 519895\r\nCache-Control: max-age=604800\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Thu, 22 Feb 2024 09:51:08 GMT\r\nEtag: \"3147526947+ident\"\r\nExpires: Thu, 29 Feb 2024 09:51:08 GMT\r\nLast-Modified: Thu, 17 Oct 2019 07:18:26 GMT\r\nServer: ECS (dce/26A0)\r\nVary: Accept-Encoding\r\nX-Cache: HIT\r\nContent-Length: 1256\r\nConnection: close\r\n\r\n<!doctype html>\n<html>\n<head>\n <title>XXXXXXXXXXXXXX</title>\n\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n body {\n background-color: #f0f0f2;\n margin: 0;\n padding: 0;\n font-family: -apple-system, system-ui, BlinkMacSystemFont, \"Segoe UI\", \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n \n }\n div {\n width: 600px;\n margin: 5em auto;\n padding: 2em;\n background-color: #fdfdff;\n border-radius: 0.5em;\n box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);\n }\n a:link, a:visited {\n color: #38488f;\n text-decoration: none;\n }\n @media (max-width: 700px) {\n div {\n margin: 0 auto;\n width: auto;\n }\n }\n </style> \n</head>\n\n<body>\n<div>\n <h1>XXXXXXXXXXXXXX</h1>\n <p>This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.</p>\n <p><a href=\"https://www.iana.org/domains/example\">More information...</a></p>\n</div>\n</body>\n</html>\n",
"notaryUrl": "http://localhost"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,162 @@
import {
Prover as _Prover,
NotaryServer,
Presentation as _Presentation,
Commit,
mapStringToRange,
subtractRanges,
Transcript,
Reveal,
} from '../../src/lib';
import * as Comlink from 'comlink';
import { HTTPParser } from 'http-parser-js';
const { init, Prover, Presentation }: any = Comlink.wrap(
// @ts-ignore
new Worker(new URL('../worker.ts', import.meta.url)),
);
(async function () {
try {
await init({ loggingLevel: 'Debug' });
// @ts-ignore
console.log('test start');
console.time('prove');
const prover = (await new Prover({
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());
// 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',
},
});
const transcript = await prover.transcript();
const { sent, recv } = transcript;
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]}`,
`"id": ${body.id}`,
`"city": "${body.information.address.city}"`,
`"postalCode": "12345"`,
],
Buffer.from(recv).toString('utf-8'),
),
],
};
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: 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();
console.time('verify');
const { transcript: partialTranscript, server_name } =
await presentation.verify();
const verifyingKey = await presentation.verifyingKey();
console.timeEnd('verify');
console.log('verifyingKey', verifyingKey);
const t = new Transcript({
sent: partialTranscript.sent,
recv: partialTranscript.recv,
});
const sentStr = t.sent();
const recvStr = t.recv();
console.log("Sent:", sentStr);
console.log("Received:", recvStr);
// @ts-ignore
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').textContent = err.message;
}
})();
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,
};
}

File diff suppressed because one or more lines are too long

View File

@@ -1,92 +0,0 @@
import {
Prover as _Prover,
NotaryServer,
Presentation as _Presentation,
} from '../../src/lib';
import * as Comlink from 'comlink';
import { Transcript } from '../../src/lib';
import { assert } from '../utils';
const { init, Prover, Presentation }: any = Comlink.wrap(
// @ts-ignore
new Worker(new URL('../worker.ts', import.meta.url)),
);
(async function () {
try {
await init({ loggingLevel: 'Debug' });
// @ts-ignore
console.log('test start');
console.time('prove');
const prover = (await new Prover({
id: 'test',
serverDns: 'swapi.dev',
})) as _Prover;
const notary = NotaryServer.from('http://localhost: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',
headers: {
'content-type': 'application/json',
secret: 'test_secret',
},
});
const transcript = await prover.transcript();
console.log({ transcript });
const commit = {
sent: [
...Object.entries(transcript.ranges.sent.headers)
.filter(([k]) => k !== 'secret')
.map(([, v]) => v),
transcript.ranges.sent.info,
...transcript.ranges.sent.lineBreaks,
],
recv: [
...Object.entries(transcript.ranges.recv.headers).map(([, v]) => v),
transcript.ranges.recv.info,
...transcript.ranges.recv.lineBreaks,
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['hair_color'],
transcript.ranges.recv.json!['skin_color'],
],
};
console.log(commit);
const notarizationOutput = await prover.notarize(commit);
const presentation = (await new Presentation({
attestationHex: notarizationOutput.attestation,
secretsHex: notarizationOutput.secrets,
reveal: commit,
})) as _Presentation;
console.log('presentation:', await presentation.serialize());
console.timeEnd('prove');
console.time('verify');
const { transcript: partialTranscript, server_name } =
await presentation.verify();
const verifyingKey = await presentation.verifyingKey();
console.timeEnd('verify');
console.log('verifyingKey', verifyingKey);
const t = new Transcript({
sent: partialTranscript.sent,
recv: partialTranscript.recv,
});
const sent = t.sent();
const recv = t.recv();
assert(sent.includes('host: swapi.dev'));
assert(!sent.includes('secret: test_secret'));
assert(recv.includes('"name":"Luke Skywalker"'));
assert(recv.includes('"hair_color":"blond"'));
assert(recv.includes('"skin_color":"fair"'));
assert(server_name === 'swapi.dev');
// @ts-ignore
document.getElementById('full-integration-swapi').textContent = 'OK';
} catch (err) {
console.log('caught error from wasm');
console.error(err);
// @ts-ignore
document.getElementById('full-integration-swapi').textContent = err.message;
}
})();

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -1,179 +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 yaml = require('js-yaml');
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;
let tlsnServerFixture: ChildProcess;
const spawnTlsnServerFixture = () => {
const tlsnServerFixturePath = './utils/tlsn/crates/server-fixture/';
tlsnServerFixture = exec(`../../target/release/tlsn-server-fixture`, {
cwd: tlsnServerFixturePath,
});
tlsnServerFixture.on('error', (error) => {
console.error(`Failed to start TLSN Server Fixture: ${error}`);
process.exit(1);
});
tlsnServerFixture.stdout?.on('data', (data) => {
console.log(`Server: ${data}`);
});
tlsnServerFixture.stderr?.on('data', (data) => {
console.error(`Server Error: ${data}`);
});
};
let localNotaryServer: ChildProcess;
const spawnLocalNotaryServer = async () => {
const localNotaryServerPath = './utils/tlsn/crates/notary/server';
console.log(localNotaryServerPath);
localNotaryServer = exec(`../../../target/release/notary-server`, {
cwd: localNotaryServerPath,
});
localNotaryServer.on('error', (error) => {
console.error(`Failed to start Notary server: ${error}`);
process.exit(1);
});
localNotaryServer.stdout?.on('data', (data) => {
console.log(`Server: ${data}`);
});
localNotaryServer.stderr?.on('data', (data) => {
console.error(`Server Error: ${data}`);
});
// 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));
}
};
const configureNotaryServer = () => {
try {
const configPath = './utils/tlsn/crates/notary/server/config/config.yaml';
const fileContents = fs.readFileSync(configPath, 'utf8');
const data = yaml.load(fileContents) as any;
data.tls.enabled = false;
data.server.host = '127.0.0.1';
const newYaml = yaml.dump(data);
fs.writeFileSync(configPath, newYaml, 'utf8');
console.log('YAML file has been updated.');
} catch (error) {
console.error('Error reading or updating the YAML file:', error);
}
};
// expose variables
before(async function () {
server = exec('serve --config ../serve.json ./test-build -l 3001');
spawnTlsnServerFixture();
configureNotaryServer(); //TODO: After alpha.8: remove this and add as argument to notary server
await spawnLocalNotaryServer();
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 {
tlsnServerFixture.kill();
console.log('* Stopped TLSN Server Fixture ✅');
localNotaryServer.kill();
console.log('* Stopped Notary Server ✅');
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');
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, 'specs')).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();
}

View File

@@ -1,3 +0,0 @@
export function assert(expr: any, msg = 'unknown assertion error') {
if (!Boolean(expr)) throw new Error(msg);
}

View File

@@ -1 +1,2 @@
tlsn
pkg

View File

@@ -1,5 +1,4 @@
#!/bin/bash
# Run tlsn Server fixture
set -e # Exit on error
@@ -8,6 +7,8 @@ cd "$(dirname "$0")"
VERSION=${1:-origin/dev} # use `dev` branch if no version is set
rm -rf pkg
# Name of the directory where the repo will be cloned
REPO_DIR="tlsn"
@@ -27,11 +28,9 @@ fi
git checkout "${VERSION}" --force
git reset --hard
for dir in "crates/server-fixture/" "crates/notary/server"; do
# Change to the specific subdirectory
cd ${dir}
cd crates/wasm
cargo update
./build.sh
cd ../../
# Build the project
cargo build --release
cd -
done
cp -r crates/wasm/pkg ..

View File

@@ -1,6 +1,7 @@
const webpack = require('webpack');
const path = require('path');
const isProd = process.env.NODE_ENV === 'production';
const CopyWebpackPlugin = require('copy-webpack-plugin');
const envPlugin = new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
@@ -53,6 +54,30 @@ module.exports = [
},
plugins: [
envPlugin,
new CopyWebpackPlugin({
patterns: [
{
from: 'node_modules/tlsn-wasm/tlsn_wasm.js',
to: path.join(__dirname, 'build'),
force: true,
},
{
from: 'node_modules/tlsn-wasm/tlsn_wasm_bg.wasm',
to: path.join(__dirname, 'build'),
force: true,
},
{
from: 'node_modules/tlsn-wasm/spawn.js',
to: path.join(__dirname, 'build'),
force: true,
},
{
from: 'node_modules/tlsn-wasm/snippets',
to: path.join(__dirname, 'build', 'snippets'),
force: true,
},
],
}),
],
},
];

View File

@@ -1,6 +1,7 @@
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const isProd = process.env.NODE_ENV === 'production';
@@ -32,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', 'specs', 'full-integration-swapi.spec.ts'),
'simple-verify': path.join(__dirname, 'test', 'specs', 'simple-verify.spec.ts'),
},
entry,
output: {
path: __dirname + '/test-build',
publicPath: '/',
@@ -48,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],
@@ -79,10 +64,50 @@ module.exports = [
new webpack.ProvidePlugin({
process: 'process',
}),
new CopyWebpackPlugin({
patterns: [
{
from: 'node_modules/tlsn-wasm',
to: path.join(__dirname, 'test-build'),
force: true,
},
],
}),
// 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',