From ec3be0d56133e1de91128f49b8f369700a2bec3e Mon Sep 17 00:00:00 2001 From: Hendrik Eeckhaut Date: Fri, 5 Apr 2024 23:24:17 +0200 Subject: [PATCH] WIP: interactive prover in TypeScript/React --- interactive-demo/prover-ts/.gitignore | 1 + interactive-demo/prover-ts/README.md | 15 ++ interactive-demo/prover-ts/app.tsx | 90 +++++++++ interactive-demo/prover-ts/index.ejs | 16 ++ interactive-demo/prover-ts/package.json | 30 +++ interactive-demo/prover-ts/tsconfig.json | 19 ++ interactive-demo/prover-ts/webpack.js | 110 +++++++++++ interactive-demo/prover/prover-rs/src/main.rs | 6 +- interactive-demo/verifier/src/lib.rs | 5 +- package.json | 11 +- src/index.ts | 19 ++ src/tlsn.ts | 26 +++ wasm/prover/Cargo.toml | 4 +- wasm/prover/src/interactive_prover.rs | 173 ++++++++++++++++++ wasm/prover/src/lib.rs | 3 + 15 files changed, 517 insertions(+), 11 deletions(-) create mode 100644 interactive-demo/prover-ts/.gitignore create mode 100644 interactive-demo/prover-ts/README.md create mode 100644 interactive-demo/prover-ts/app.tsx create mode 100644 interactive-demo/prover-ts/index.ejs create mode 100644 interactive-demo/prover-ts/package.json create mode 100644 interactive-demo/prover-ts/tsconfig.json create mode 100644 interactive-demo/prover-ts/webpack.js create mode 100644 wasm/prover/src/interactive_prover.rs diff --git a/interactive-demo/prover-ts/.gitignore b/interactive-demo/prover-ts/.gitignore new file mode 100644 index 0000000..d8b83df --- /dev/null +++ b/interactive-demo/prover-ts/.gitignore @@ -0,0 +1 @@ +package-lock.json diff --git a/interactive-demo/prover-ts/README.md b/interactive-demo/prover-ts/README.md new file mode 100644 index 0000000..054d1e7 --- /dev/null +++ b/interactive-demo/prover-ts/README.md @@ -0,0 +1,15 @@ +TODO + +1. Start Verifier +2. Start websocket proxy +3. Start app + + +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 websockify proxy **locally**, run: +```sh +git clone https://github.com/novnc/websockify && cd websockify +./docker/build.sh +docker run -it --rm -p 55688:80 novnc/websockify 80 swapi.dev:443 +``` \ No newline at end of file diff --git a/interactive-demo/prover-ts/app.tsx b/interactive-demo/prover-ts/app.tsx new file mode 100644 index 0000000..3f2c1c3 --- /dev/null +++ b/interactive-demo/prover-ts/app.tsx @@ -0,0 +1,90 @@ +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { interactive_prove, prove, verify } from 'tlsn-js'; +import { Proof } from 'tlsn-js/build/types'; +import { Watch } from 'react-loader-spinner'; + +const container = document.getElementById('root'); +const root = createRoot(container!); + +root.render(); + +function App(): ReactElement { + const [processing, setProcessing] = useState(false); + const [result, setResult] = useState<{ + time: number; + sent: string; + recv: string; + notaryUrl: string; + } | null>(null); + const [proof, setProof] = useState(null); + + const uri = "https://swapi.dev/api/people/1"; + const id = "interactive-verifier-demo"; + + const onClick = useCallback(async () => { + setProcessing(true); + const p = await interactive_prove( + 'ws://localhost:55688', + 'ws://localhost:9816', + uri, + id); + // setProof(p); + }, [setProof, setProcessing]); + + useEffect(() => { + (async () => { + if (proof) { + const r = await verify(proof); + setResult(r); + setProcessing(false); + } + })(); + }, [proof, setResult]); + + return ( +
+ +
+ Proof: + {!processing && !proof ? ( + not started + ) : !proof ? ( + <> + Proving data from swapi... + + Open Developer tools to follow progress + + ) : ( + <> +
+ View Proof +
{JSON.stringify(proof, null, 2)}
+
+ + )} +
+
+ Verification: + {!proof ? ( + not started + ) : !result ? ( + verifying + ) : ( +
{JSON.stringify(result, null, 2)}
+ )} +
+
+ ); +} diff --git a/interactive-demo/prover-ts/index.ejs b/interactive-demo/prover-ts/index.ejs new file mode 100644 index 0000000..7bd6dac --- /dev/null +++ b/interactive-demo/prover-ts/index.ejs @@ -0,0 +1,16 @@ + + + + + + + React/Typescrip Example + + + + +
+ + + \ No newline at end of file diff --git a/interactive-demo/prover-ts/package.json b/interactive-demo/prover-ts/package.json new file mode 100644 index 0000000..4bb6399 --- /dev/null +++ b/interactive-demo/prover-ts/package.json @@ -0,0 +1,30 @@ +{ + "name": "prover-ts", + "version": "1.0.0", + "description": "", + "main": "webpack.js", + "scripts": { + "dev": "webpack-dev-server --config webpack.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-loader-spinner": "^6.1.6", + "tlsn-js": "../../../tlsn-js" + }, + "devDependencies": { + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.10", + "babel-loader": "^9.1.3", + "copy-webpack-plugin": "^11.0.0", + "html-webpack-plugin": "^5.5.0", + "source-map-loader": "^5.0.0", + "ts-loader": "^9.4.2", + "typescript": "^4.9.4", + "webpack": "^5.75.0", + "webpack-cli": "^4.10.0", + "webpack-dev-server": "^4.11.1" + } +} \ No newline at end of file diff --git a/interactive-demo/prover-ts/tsconfig.json b/interactive-demo/prover-ts/tsconfig.json new file mode 100644 index 0000000..226acce --- /dev/null +++ b/interactive-demo/prover-ts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "noEmit": false, + "jsx": "react" + }, + "include": ["app.tsx"] +} \ No newline at end of file diff --git a/interactive-demo/prover-ts/webpack.js b/interactive-demo/prover-ts/webpack.js new file mode 100644 index 0000000..bf71b21 --- /dev/null +++ b/interactive-demo/prover-ts/webpack.js @@ -0,0 +1,110 @@ +var webpack = require('webpack'), + path = require('path'), + CopyWebpackPlugin = require('copy-webpack-plugin'), + HtmlWebpackPlugin = require('html-webpack-plugin'); + +const ASSET_PATH = process.env.ASSET_PATH || '/'; + +var alias = {}; + +var fileExtensions = [ + 'jpg', + 'jpeg', + 'png', + 'gif', + 'eot', + 'otf', + 'svg', + 'ttf', + 'woff', + 'woff2', +]; + +var options = { + ignoreWarnings: [ + /Circular dependency between chunks with runtime/, + /ResizeObserver loop completed with undelivered notifications/, + ], + mode: 'development', + entry: { + app: path.join(__dirname, 'app.tsx'), + }, + output: { + filename: '[name].bundle.js', + path: path.resolve(__dirname, 'build'), + clean: true, + publicPath: ASSET_PATH, + }, + module: { + rules: [ + { + test: new RegExp('.(' + fileExtensions.join('|') + ')$'), + type: 'asset/resource', + exclude: /node_modules/, + }, + { + test: /\.html$/, + loader: 'html-loader', + exclude: /node_modules/, + }, + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve('ts-loader'), + }, + ], + }, + { + test: /\.(js|jsx)$/, + use: [ + { + loader: 'source-map-loader', + }, + { + loader: require.resolve('babel-loader'), + }, + ], + exclude: /node_modules/, + }, + ], + }, + resolve: { + alias: alias, + extensions: fileExtensions + .map((extension) => '.' + extension) + .concat(['.js', '.jsx', '.ts', '.tsx', '.css']), + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: 'node_modules/tlsn-js/build', + to: path.join(__dirname, 'build'), + force: true, + }, + ], + }), + new HtmlWebpackPlugin({ + template: path.join(__dirname, 'index.ejs'), + filename: 'index.html', + cache: false, + }), + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }), + ].filter(Boolean), + // Required by wasm-bindgen-rayon, in order to use SharedArrayBuffer on the Web + // Ref: + // - https://github.com/GoogleChromeLabs/wasm-bindgen-rayon#setting-up + // - https://web.dev/i18n/en/coop-coep/ + devServer: { + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + }, + }, +}; + +module.exports = options; diff --git a/interactive-demo/prover/prover-rs/src/main.rs b/interactive-demo/prover/prover-rs/src/main.rs index 61af73b..d40180e 100644 --- a/interactive-demo/prover/prover-rs/src/main.rs +++ b/interactive-demo/prover/prover-rs/src/main.rs @@ -149,12 +149,12 @@ fn redact_and_reveal_received_data(prover: &mut Prover) { // Get the homeworld from the received data. let received_string = String::from_utf8(prover.recv_transcript().data().to_vec()).unwrap(); let re = Regex::new(r#""homeworld"\s?:\s?"(.*?)""#).unwrap(); - let commit_hash_match = re.captures(&received_string).unwrap().get(1).unwrap(); + let homeworld_match = re.captures(&received_string).unwrap().get(1).unwrap(); // Reveal everything except for the homeworld. - _ = prover.reveal(0..commit_hash_match.start(), Direction::Received); + _ = prover.reveal(0..homeworld_match.start(), Direction::Received); _ = prover.reveal( - commit_hash_match.end()..recv_transcript_len, + homeworld_match.end()..recv_transcript_len, Direction::Received, ); } diff --git a/interactive-demo/verifier/src/lib.rs b/interactive-demo/verifier/src/lib.rs index 94eb634..abc95f8 100644 --- a/interactive-demo/verifier/src/lib.rs +++ b/interactive-demo/verifier/src/lib.rs @@ -124,12 +124,14 @@ async fn verifier( let verifier = Verifier::new(verifier_config); // Verify MPC-TLS and wait for (redacted) data. + debug!("Starting MPC-TLS verification..."); let (sent, received, session_info) = verifier .verify(socket.compat()) .await .map_err(|err| eyre!("Verification failed: {err}"))?; - // Check send data: check host. + // Check sent data: check host. + debug!("Starting sent data verification..."); let sent_data = String::from_utf8(sent.data().to_vec()) .map_err(|err| eyre!("Failed to parse sent data: {err}"))?; sent_data @@ -137,6 +139,7 @@ async fn verifier( .ok_or_else(|| eyre!("Verification failed: Expected host {}", server_domain))?; // Check received data: check json and version number. + debug!("Starting received data verification..."); let response = String::from_utf8(received.data().to_vec()) .map_err(|err| eyre!("Failed to parse received data: {err}"))?; debug!("Received data: {:?}", response); diff --git a/package.json b/package.json index 983e7c1..5553c38 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "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": "NODE_ENV=production concurrently npm:build:src npm:build:types", + "build": "npm run build:wasm && NODE_ENV=production concurrently npm:build:src npm:build:types", "update:wasm": "sh utils/check-wasm.sh -f", "test:wasm": "cd wasm/prover; wasm-pack test --firefox --release --headless", "build:wasm": "wasm-pack build --target web wasm/prover", @@ -38,14 +38,12 @@ "@types/expect": "^24.3.0", "@types/mocha": "^10.0.6", "@types/serve-handler": "^6.1.4", - "@typescript-eslint/eslint-plugin": "^6.15.0", - "@typescript-eslint/parser": "7.0.2", "browserify": "^17.0.0", "concurrently": "^5.1.0", "constants-browserify": "^1.0.0", "copy-webpack-plugin": "^5.0.5", "crypto-browserify": "^3.12.0", - "eslint": "^8.47.0", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "file-loader": "^5.0.2", @@ -64,7 +62,8 @@ "ts-loader": "^6.2.1", "ts-mocha": "^10.0.0", "ts-node": "^10.9.2", - "typescript": "^4.9.3", + "typescript": "^4.9.5", + "typescript-eslint": "^7.4.0", "webpack": "^5.75.0", "webpack-cli": "^5.0.0", "webpack-dev-server": "^4.11.1", @@ -76,4 +75,4 @@ "engines": { "node": ">= 16.20.2" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6e6471a..666bcb4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,6 +56,25 @@ export const prove = async ( }; }; + +export const interactive_prove = async ( + websocket_proxy_url: string, + verifier_proxy_url: string, + uri: string, + id: string +): Promise => { + const tlsn = await getTLSN(); + + + const proof = await tlsn.interactive_prove( + websocket_proxy_url, + verifier_proxy_url, + uri, + id); + + return proof +}; + export const verify = async ( proof: Proof, publicKeyOverride?: string, diff --git a/src/tlsn.ts b/src/tlsn.ts index 17cfb17..2bf0ef0 100644 --- a/src/tlsn.ts +++ b/src/tlsn.ts @@ -1,6 +1,7 @@ import init, { initThreadPool, prover, + interactive_prover, verify, } from '../wasm/prover/pkg/tlsn_extension_rs'; @@ -73,6 +74,31 @@ export default class TLSN { return resJSON; } + async interactive_prove( + websocket_proxy_url: string, + verifier_proxy_url: string, + uri: string, + id: string, + ) { + await this.waitForStart(); + const resProver = await interactive_prover( + websocket_proxy_url, + verifier_proxy_url, + uri, + id + ); + const resJSON = JSON.parse(resProver); + // console.log('!@# resProver,resJSON=', { resProver, resJSON }); + // console.log('!@# resAfter.memory=', resJSON.memory); + // 1105920000 ~= 1.03 gb + // console.log( + // '!@# resAfter.memory.buffer.length=', + // resJSON.memory?.buffer?.byteLength, + // ); + + return resJSON; + } + async verify(proof: any, pubkey: string) { await this.waitForStart(); const raw = await verify(JSON.stringify(proof), pubkey); diff --git a/wasm/prover/Cargo.toml b/wasm/prover/Cargo.toml index cba3959..e71cfb2 100644 --- a/wasm/prover/Cargo.toml +++ b/wasm/prover/Cargo.toml @@ -31,7 +31,7 @@ pin-project-lite = "0.2.4" http-body-util = "0.1" hyper = { version = "1.1", features = ["client", "http1"] } -hyper-util = { version = "0.1", features = ["http1"] } +hyper-util = { version = "0.1.3" } tracing-subscriber = { version = "0.3", features = ["time"] } tracing-web = "0.1.2" @@ -81,6 +81,7 @@ console_error_panic_hook = { version = "0.1.7" } strum = { version = "0.26.1" } strum_macros = "0.26.1" +regex = "1.10.4" [dev-dependencies] wasm-bindgen-test = "0.3.34" @@ -88,6 +89,7 @@ wasm-bindgen-test = "0.3.34" [profile.release] lto = true # Enable Link Time Optimization opt-level = "z" # Optimize for size +debug = true [package.metadata.wasm-pack.profile.release] wasm-opt = true diff --git a/wasm/prover/src/interactive_prover.rs b/wasm/prover/src/interactive_prover.rs new file mode 100644 index 0000000..f4d5f55 --- /dev/null +++ b/wasm/prover/src/interactive_prover.rs @@ -0,0 +1,173 @@ +use crate::hyper_io::FuturesIo; +use crate::request_opt::RequestOptions; +pub use crate::request_opt::VerifyResult; +use crate::requests::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse}; +use crate::{fetch_as_json_string, setup_tracing_web}; +use futures::channel::oneshot; +use futures::AsyncWriteExt; +use http_body_util::{BodyExt, Empty, Full}; +use hyper::{body::Bytes, Request, StatusCode, Uri}; +use js_sys::Array; +use regex::Regex; +use std::ops::Range; +use strum::EnumMessage; +use tlsn_core::proof::TlsProof; +use tlsn_core::{proof::SessionInfo, Direction, RedactedTranscript}; +use tlsn_prover::tls::{state::Prove, Prover, ProverConfig}; +use tracing::instrument; +use url::Url; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; +pub use wasm_bindgen_rayon::init_thread_pool; +use web_sys::{Headers, RequestInit, RequestMode}; +use web_time::Instant; +use ws_stream_wasm::*; + +use tracing::{debug, info}; + +const SECRET: &str = "TLSNotary's private key 🤡"; + +#[tracing::instrument] +#[wasm_bindgen] +pub async fn interactive_prover( + websocket_proxy_url: String, + verifier_proxy_url: String, + uri: String, + id: String, +) -> Result { + let uri = uri.parse::().unwrap(); + assert_eq!(uri.scheme().unwrap().as_str(), "https"); + let server_domain = uri.authority().unwrap().host(); + + info!( + "Interactive proof: {}, {}, {} ,{} ", + websocket_proxy_url, verifier_proxy_url, uri, id + ); + + let test = format!("{}/verify", verifier_proxy_url); + let (_, verifier_ws_stream) = WsMeta::connect(test, None) + .await + .expect_throw("assume the verifier ws connection succeeds"); + let verifier_ws_stream_into = verifier_ws_stream.into_io(); + + // Create prover and connect to verifier. + let prover = Prover::new( + ProverConfig::builder() + .id(id) + .server_dns(server_domain) + .build() + .unwrap(), + ) + .setup(verifier_ws_stream_into) + .await + .unwrap(); + + // Connect to TLS Server. + debug!("Connect to websocket proxy {}", websocket_proxy_url); + let (_, client_ws_stream) = WsMeta::connect(websocket_proxy_url, None) + .await + .expect_throw("assume the client ws connection succeeds"); + let (mpc_tls_connection, prover_fut) = + prover.connect(client_ws_stream.into_io()).await.unwrap(); + let mpc_tls_connection = unsafe { FuturesIo::new(mpc_tls_connection) }; + let (prover_sender, prover_receiver) = oneshot::channel(); + let handled_prover_fut = async { + let result = prover_fut.await; + let _ = prover_sender.send(result); + }; + spawn_local(handled_prover_fut); + + // Attach the hyper HTTP client to the TLS connection + let (mut request_sender, connection) = + hyper::client::conn::http1::handshake(mpc_tls_connection) + .await + .map_err(|e| JsValue::from_str(&format!("Could not handshake: {:?}", e)))?; + + // Spawn the HTTP task to be run concurrently + let (connection_sender, connection_receiver) = oneshot::channel(); + let connection_fut = connection.without_shutdown(); + let handled_connection_fut = async { + let result = connection_fut.await; + let _ = connection_sender.send(result); + }; + spawn_local(handled_connection_fut); + + // MPC-TLS: Send Request and wait for Response. + let request = Request::builder() + .uri(uri.clone()) + .header("Host", server_domain) + .header("Connection", "close") + .header("Secret", SECRET) + .method("GET") + .body(Empty::::new()) + .unwrap(); + let response = request_sender.send_request(request).await.unwrap(); + + assert!(response.status() == StatusCode::OK); + + // Close TLS Connection. + // let mut client_socket = connection_receiver + // .await + // .map_err(|e| { + // JsValue::from_str(&format!( + // "Could not receive from connection_receiver: {:?}", + // e + // )) + // })? + // .map_err(|e| JsValue::from_str(&format!("Could not get TlsConnection: {:?}", e)))? + // .io + // .into_inner(); + // client_socket + // .close() + // .await + // .map_err(|e| JsValue::from_str(&format!("Could not close socket: {:?}", e)))?; + + // Create proof for the Verifier. + let prover = prover_receiver + .await + .map_err(|e| { + JsValue::from_str(&format!("Could not receive from prover_receiver: {:?}", e)) + })? + .map_err(|e| JsValue::from_str(&format!("Could not get Prover: {:?}", e)))?; + let mut prover = prover.start_prove(); + redact_and_reveal_received_data(&mut prover); + redact_and_reveal_sent_data(&mut prover); + prover.prove().await.unwrap(); + + // Finalize. + let _ = prover.finalize().await; + + Ok(r#"{"result": "success"}"#.to_string()) +} + +/// Redacts and reveals received data to the verifier. +fn redact_and_reveal_received_data(prover: &mut Prover) { + let recv_transcript_len = prover.recv_transcript().data().len(); + + // Get the homeworld from the received data. + let received_string = String::from_utf8(prover.recv_transcript().data().to_vec()).unwrap(); + let re = Regex::new(r#""homeworld"\s?:\s?"(.*?)""#).unwrap(); + let commit_hash_match = re.captures(&received_string).unwrap().get(1).unwrap(); + + // Reveal everything except for the commit hash. + _ = prover.reveal(0..commit_hash_match.start(), Direction::Received); + _ = prover.reveal( + commit_hash_match.end()..recv_transcript_len, + Direction::Received, + ); +} + +/// Redacts and reveals sent data to the verifier. +fn redact_and_reveal_sent_data(prover: &mut Prover) { + let sent_transcript_len = prover.sent_transcript().data().len(); + + let sent_string = String::from_utf8(prover.sent_transcript().data().to_vec()).unwrap(); + let secret_start = sent_string.find(SECRET).unwrap(); + + // Reveal everything except for the SECRET. + _ = prover.reveal(0..secret_start, Direction::Sent); + _ = prover.reveal( + secret_start + SECRET.len()..sent_transcript_len, + Direction::Sent, + ); +} diff --git a/wasm/prover/src/lib.rs b/wasm/prover/src/lib.rs index 934d36c..e6f0a27 100644 --- a/wasm/prover/src/lib.rs +++ b/wasm/prover/src/lib.rs @@ -5,6 +5,9 @@ mod requests; pub mod prover; pub use prover::prover; +pub mod interactive_prover; +pub use interactive_prover::interactive_prover; + pub mod verify; use tracing::error; pub use verify::verify;