feat: upgrade to alpha.6 and new tlsn-wasm prover (#61)

* Revert "chore: release v0.1.0-alpha.6 (#57)"

This reverts commit 292b4263d7.

* parsed json

* add json commitments

* parse json from transcript

* wip

* wip

* wip

* feat: update to alpha.6

* chore: commit wasm pkg directory

* chore: version nump

* fix: test suite for alpha.6

* fix: remove wasm build from ci

* fix: update pnpm lockfile

* fix: remove test:wasm

* fix: linter and add new devDependency
This commit is contained in:
tsukino
2024-08-12 05:52:19 -04:00
committed by GitHub
parent 292b4263d7
commit e4a8a9c723
47 changed files with 41619 additions and 1482 deletions

View File

@@ -53,18 +53,12 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install wasm-pack
run: npm install -g wasm-pack
- 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 WASM
run: npm run build:wasm
- name: Build
run: npm run build

View File

@@ -65,21 +65,12 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install wasm-pack
run: npm install -g wasm-pack
- 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 WASM
run: npm run build:wasm
- name: Test WASM
run: npm run test:wasm
- name: Build Test dependencies
run: npm run build:tlsn-binaries

6
.gitignore vendored
View File

@@ -1,6 +1,5 @@
wasm/prover/pkg
wasm/prover/target
wasm/prover/Cargo.lock
wasm-pack.log
node_modules/
.idea/
@@ -9,7 +8,4 @@ build/
dev-build/
test-build/
./demo/node_modules
./demo/package-lock.json
package-lock.json
pnpm-lock.yaml
yarn.lock
utils/tlsn

View File

@@ -1,89 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { prove, verify, set_logging_filter } 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(<App />);
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<Proof | null>(null);
const onClick = useCallback(async () => {
setProcessing(true);
await set_logging_filter('info,tlsn_extension_rs=debug');
const p = await prove('https://swapi.dev/api/people/1', {
method: 'GET',
maxTranscriptSize: 16384,
notaryUrl: 'http://localhost:7047',
websocketProxyUrl: 'ws://localhost:55688',
});
setProof(p);
}, [setProof, setProcessing]);
useEffect(() => {
(async () => {
if (proof) {
const r = await verify(proof);
setResult(r);
setProcessing(false);
}
})();
}, [proof, setResult]);
return (
<div>
<button onClick={!processing ? onClick : undefined} disabled={processing}>
Start demo
</button>
<div>
<b>Proof: </b>
{!processing && !proof ? (
<i>not started</i>
) : !proof ? (
<>
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(proof, null, 2)}</pre>
</details>
</>
)}
</div>
<div>
<b>Verification: </b>
{!proof ? (
<i>not started</i>
) : !result ? (
<i>verifying</i>
) : (
<pre>{JSON.stringify(result, null, 2)}</pre>
)}
</div>
</div>
);
}

View File

@@ -4,11 +4,13 @@
"description": "",
"main": "webpack.js",
"scripts": {
"dev": "webpack-dev-server --config webpack.js"
"dev": "webpack-dev-server --config webpack.js",
"build": "webpack --config webpack.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"comlink": "^4.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
@@ -19,12 +21,15 @@
"@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",
"source-map-loader": "^5.0.0",
"stream-browserify": "^3.0.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"vm-browserify": "^1.1.2",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}
}

3428
demo/react-ts-webpack/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,150 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import * as Comlink from 'comlink';
import { Watch } from 'react-loader-spinner';
import {
Prover as TProver,
NotarizedSession as TNotarizedSession,
TlsProof as TTlsProof,
Commit,
NotaryServer,
ProofData,
} from 'tlsn-js';
const { init, Prover, NotarizedSession, TlsProof }: 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<ProofData | null>(null);
const [proofHex, setProofHex] = useState<null | string>(null);
const onClick = useCallback(async () => {
setProcessing(true);
const notary = NotaryServer.from(`http://localhost:7047`);
console.time('submit');
await init({ loggingLevel: 'Debug' });
const prover = (await new Prover({
serverDns: 'swapi.dev',
})) as TProver;
await prover.setup(await notary.sessionUrl());
const resp = 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,
},
});
console.timeEnd('submit');
console.log(resp);
console.time('transcript');
const transcript = await prover.transcript();
console.log(transcript);
console.timeEnd('transcript');
console.time('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,
],
};
console.log(commit);
const session = await prover.notarize(commit);
console.timeEnd('commit');
console.time('proof');
const notarizedSession = (await new NotarizedSession(
session,
)) as TNotarizedSession;
const proofHex = await notarizedSession.proof(commit);
console.timeEnd('proof');
setProofHex(proofHex);
}, [setProofHex, setProcessing]);
useEffect(() => {
(async () => {
if (proofHex) {
const proof = (await new TlsProof(proofHex)) as TTlsProof;
const notary = NotaryServer.from(`http://localhost:7047`);
const notaryKey = await notary.publicKey();
const proofData = await proof.verify({
typ: 'P256',
key: notaryKey,
});
setResult(proofData);
setProcessing(false);
}
})();
}, [proofHex, setResult]);
return (
<div>
<button onClick={!processing ? onClick : undefined} disabled={processing}>
Start demo
</button>
<div>
<b>Proof: </b>
{!processing && !proofHex ? (
<i>not started</i>
) : !proofHex ? (
<>
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(proofHex, null, 2)}</pre>
</details>
</>
)}
</div>
<div>
<b>Verification: </b>
{!proofHex ? (
<i>not started</i>
) : !result ? (
<i>verifying</i>
) : (
<pre>{JSON.stringify(result, null, 2)}</pre>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import * as Comlink from 'comlink';
import init, { Prover, NotarizedSession, TlsProof } from 'tlsn-js';
Comlink.expose({
init,
Prover,
NotarizedSession,
TlsProof,
});

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
@@ -15,5 +15,7 @@
"noEmit": false,
"jsx": "react"
},
"include": ["app.tsx"]
"include": [
"src/app.tsx"
]
}

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'),
},
@@ -75,6 +78,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({
@@ -91,6 +99,9 @@ var options = {
filename: 'index.html',
cache: false,
}),
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),

26760
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "tlsn-js",
"version": "v0.1.0-alpha.6.0",
"version": "v0.1.0-alpha.6.1",
"description": "",
"repository": "https://github.com/tlsnotary/tlsn-js",
"main": "build/index.js",
"types": "build/index.d.ts",
"main": "build/lib.js",
"types": "build/lib.d.ts",
"files": [
"build/",
"src/",
@@ -16,30 +16,27 @@
"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:lib": "NODE_ENV=production concurrently npm:build:src npm:build:types",
"build": "npm run build:wasm && npm run build:lib",
"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",
"build:tlsn-binaries": "sh utils/build-tlsn-binaries.sh",
"build:lib": "NODE_ENV=production concurrently npm:build:src npm:build:types",
"build": "npm run build:lib",
"update:wasm": "sh utils/check-wasm.sh -f",
"watch:dev": "webpack --config webpack.web.dev.config.js --watch",
"predev": "sh utils/check-wasm.sh",
"lint:wasm": "cd wasm/prover; cargo clippy --target wasm32-unknown-unknown",
"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"
},
"dependencies": {
"comlink": "^4.4.1"
"test": "npm run build:tlsn-binaries && npm run build:test && npm run run:test",
"test:only": "npm run build:test && npm run run:test"
},
"devDependencies": {
"@types/expect": "^24.3.0",
"@types/mocha": "^10.0.6",
"@types/serve-handler": "^6.1.4",
"browserify": "^17.0.0",
"buffer": "^6.0.3",
"comlink": "^4.4.1",
"concurrently": "^5.1.0",
"constants-browserify": "^1.0.0",
"copy-webpack-plugin": "^5.0.5",
@@ -75,4 +72,4 @@
"engines": {
"node": ">= 16.20.2"
}
}
}

8613
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,11 +12,11 @@ To run your own websocket proxy for `https://swapi.dev` **locally**:
1. Install [websocat](https://github.com/vi/websocat):
| tool | command |
| ------ | ------------------------------ |
| cargo | `cargo install websocat` |
| brew | `brew install websocat` |
| source | 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
@@ -31,7 +31,7 @@ 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.6"
git clone https://github.com/tlsnotary/tlsn.git --branch "v0.1.0-alpha.5"
```
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

View File

@@ -1,104 +0,0 @@
import TLSN from './tlsn';
import { DEFAULT_LOGGING_FILTER } from './tlsn';
import { Proof } from './types';
let _tlsn: TLSN;
const current_logging_filter = DEFAULT_LOGGING_FILTER;
async function getTLSN(logging_filter?: string): Promise<TLSN> {
const logging_filter_changed =
logging_filter && logging_filter == current_logging_filter;
if (!logging_filter_changed && _tlsn) return _tlsn;
// @ts-ignore
if (logging_filter) _tlsn = await new TLSN(logging_filter);
else _tlsn = await new TLSN();
return _tlsn;
}
/**
* If you want to change the default logging filter, call this method before calling prove or verify
* For the filter syntax consult: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax
* @param logging_filter
*/
export const set_logging_filter = async (logging_filter: string) => {
getTLSN(logging_filter);
};
export const prove = async (
url: string,
options: {
notaryUrl: string;
websocketProxyUrl: string;
method?: string;
headers?: { [key: string]: string };
body?: string;
maxSentData?: number;
maxRecvData?: number;
maxTranscriptSize?: number;
secretHeaders?: string[];
secretResps?: string[];
},
): Promise<Proof> => {
const {
method,
headers = {},
body = '',
maxSentData,
maxRecvData,
maxTranscriptSize = 16384,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
} = options;
const tlsn = await getTLSN();
headers['Host'] = new URL(url).host;
headers['Connection'] = 'close';
const proof = await tlsn.prove(url, {
method,
headers,
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
});
return {
...proof,
notaryUrl,
};
};
export const verify = async (
proof: Proof,
publicKeyOverride?: string,
): Promise<{
time: number;
sent: string;
recv: string;
notaryUrl: string;
}> => {
const publicKey =
publicKeyOverride || (await fetchPublicKeyFromNotary(proof.notaryUrl));
const tlsn = await getTLSN();
const result = await tlsn.verify(proof, publicKey);
return {
...result,
notaryUrl: proof.notaryUrl,
};
};
async function fetchPublicKeyFromNotary(notaryUrl: string) {
const res = await fetch(notaryUrl + '/info');
const json: any = await res.json();
if (!json.publicKey) throw new Error('invalid response');
return json.publicKey;
}

303
src/lib.ts Normal file
View File

@@ -0,0 +1,303 @@
import initWasm, {
initThreadPool,
init_logging,
LoggingLevel,
LoggingConfig,
NotarizedSession as WasmNotarizedSession,
Transcript,
TlsProof as WasmTlsProof,
type Commit,
type Reveal,
Verifier as WasmVerifier,
Prover as WasmProver,
type ProverConfig,
type Method,
VerifierConfig,
VerifierData,
NotaryPublicKey,
} from '../wasm/pkg/tlsn_wasm';
import { arrayToHex, processTranscript, stringToBuffer, expect } from './utils';
import type { ParsedTranscriptData, ProofData } from './types';
let LOGGING_LEVEL: LoggingLevel = 'Info';
function debug(...args: any[]) {
if (['Debug', 'Trace'].includes(LOGGING_LEVEL)) {
console.log('tlsn-js DEBUG', ...args);
}
}
export default async function init(config?: {
loggingLevel?: LoggingLevel;
hardwareConcurrency?: number;
}) {
const {
loggingLevel = 'Info',
hardwareConcurrency = navigator.hardwareConcurrency,
} = config || {};
LOGGING_LEVEL = loggingLevel;
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('DEBUG', 'initialize thread pool');
await initThreadPool(hardwareConcurrency);
debug('initialized thread pool');
}
export class Prover {
#prover: WasmProver;
#config: ProverConfig;
constructor(config: {
id?: string;
serverDns: string;
maxSentData?: number;
maxRecvData?: number;
}) {
this.#config = {
id: config.id || String(Date.now()),
server_dns: config.serverDns,
max_recv_data: config.maxRecvData,
max_sent_data: config.maxSentData,
};
this.#prover = new WasmProver(this.#config);
}
get id() {
return this.#config.id;
}
async free() {
return this.#prover.free();
}
async setup(verifierUrl: string): Promise<void> {
return this.#prover.setup(verifierUrl);
}
async transcript(): Promise<{
sent: string;
recv: string;
ranges: { recv: ParsedTranscriptData; sent: ParsedTranscriptData };
}> {
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),
},
};
}
async sendRequest(
wsProxyUrl: string,
request: {
url: string;
method?: Method;
headers?: { [key: string]: string };
body?: any;
},
): Promise<{
status: number;
headers: { [key: string]: string };
}> {
const { url, method = 'GET', headers = {}, body } = request;
const hostname = new URL(url).hostname;
const headerMap: Map<string, number[]> = new Map();
headerMap.set('Host', stringToBuffer(hostname));
headerMap.set('Connection', stringToBuffer('close'));
Object.entries(headers).forEach(([key, value]) => {
headerMap.set(key, stringToBuffer(value));
});
const resp = await this.#prover.send_request(wsProxyUrl, {
uri: url,
method,
headers: headerMap,
body,
});
debug('prover.sendRequest', resp);
return {
status: resp.status,
headers: resp.headers.reduce(
(acc: { [key: string]: string }, [name, arr]) => {
acc[name] = Buffer.from(arr).toString();
return acc;
},
{},
),
};
}
async notarize(commit: Commit): Promise<string> {
const notarizedSession = await this.#prover.notarize(commit);
return arrayToHex(notarizedSession.serialize());
}
}
export class Verifier {
#config: VerifierConfig;
#verifier: WasmVerifier;
constructor(config: VerifierConfig) {
this.#config = config;
this.#verifier = new WasmVerifier(this.#config);
}
get id() {
return this.#config.id;
}
async verify(): Promise<VerifierData> {
return this.#verifier.verify();
}
async connect(proverUrl: string): Promise<void> {
return this.#verifier.connect(proverUrl);
}
}
export class NotarizedSession {
#session: WasmNotarizedSession;
constructor(serializedSessionHex: string) {
this.#session = WasmNotarizedSession.deserialize(
new Uint8Array(Buffer.from(serializedSessionHex, 'hex')),
);
}
async free() {
return this.#session.free();
}
async proof(reveal: Reveal) {
const proof = this.#session.proof(reveal);
console.log(proof);
return arrayToHex(proof.serialize());
}
async serialize() {
return arrayToHex(this.#session.serialize());
}
}
export class TlsProof {
#proof: WasmTlsProof;
constructor(serializedProofHex: string) {
this.#proof = WasmTlsProof.deserialize(
new Uint8Array(Buffer.from(serializedProofHex, 'hex')),
);
}
async free() {
return this.#proof.free();
}
async serialize() {
return arrayToHex(this.#proof.serialize());
}
async verify(
notaryPublicKey: NotaryPublicKey,
redactedSymbol = '*',
): Promise<ProofData> {
const { received, received_auth_ranges, sent, ...rest } =
this.#proof.verify(notaryPublicKey);
return {
...rest,
recv_auth_ranges: received_auth_ranges,
recv: received.reduce((recv: string, num) => {
recv =
recv + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
return recv;
}, ''),
sent: sent.reduce((sent: string, num) => {
sent =
sent + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
return sent;
}, ''),
};
}
}
export class NotaryServer {
#url: string;
static from(url: string) {
return new NotaryServer(url);
}
constructor(url: string) {
this.#url = url;
}
get url() {
return this.#url;
}
async publicKey(): Promise<string> {
const res = await fetch(this.#url + '/info');
const { publicKey } = await res.json();
expect(
typeof publicKey === 'string' && !!publicKey.length,
'invalid public key',
);
return publicKey!;
}
async sessionUrl(
maxSentData?: number,
maxRecvData?: number,
): Promise<string> {
const resp = await fetch(`${this.#url}/session`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientType: 'Websocket',
maxRecvData,
maxSentData,
}),
});
const { sessionId } = await resp.json();
expect(
typeof sessionId === 'string' && !!sessionId.length,
'invalid session id',
);
const url = new URL(this.#url);
const protocol = url.protocol === 'https:' ? 'wss' : 'ws';
const pathname = url.pathname;
return `${protocol}://${url.host}${pathname === '/' ? '' : pathname}/notarize?sessionId=${sessionId!}`;
}
}
export {
type ParsedTranscriptData,
type ProofData,
type LoggingLevel,
type LoggingConfig,
type Transcript,
type Commit,
type Reveal,
type ProverConfig,
};

View File

@@ -1,96 +0,0 @@
import init, {
initThreadPool,
prover,
verify,
setup_tracing_web,
} from '../wasm/prover/pkg/tlsn_extension_rs';
export const DEFAULT_LOGGING_FILTER: string = 'info,tlsn_extension_rs=debug';
export default class TLSN {
private startPromise: Promise<void>;
private resolveStart!: () => void;
private logging_filter: string;
/**
* Initializes a new instance of the TLSN class.
*
* @param logging_filter - Optional logging filter string.
* Defaults to DEFAULT_LOGGING_FILTER
*/
constructor(logging_filter: string = DEFAULT_LOGGING_FILTER) {
this.logging_filter = logging_filter;
this.startPromise = new Promise((resolve) => {
this.resolveStart = resolve;
});
this.start();
}
async start() {
// console.log('start');
const numConcurrency = navigator.hardwareConcurrency;
// console.log('!@# navigator.hardwareConcurrency=', numConcurrency);
await init();
setup_tracing_web(this.logging_filter);
// const res = await init();
// console.log('!@# res.memory=', res.memory);
// 6422528 ~= 6.12 mb
// console.log('!@# res.memory.buffer.length=', res.memory.buffer.byteLength);
await initThreadPool(numConcurrency);
this.resolveStart();
}
async waitForStart() {
return this.startPromise;
}
async prove(
url: string,
options?: {
method?: string;
headers?: { [key: string]: string };
body?: string;
maxSentData?: number;
maxRecvData?: number;
maxTranscriptSize?: number;
notaryUrl?: string;
websocketProxyUrl?: string;
secretHeaders?: string[];
secretResps?: string[];
},
) {
await this.waitForStart();
// console.log('worker', url, {
// ...options,
// notaryUrl: options?.notaryUrl,
// websocketProxyUrl: options?.websocketProxyUrl,
// });
const resProver = await prover(
url,
{
...options,
notaryUrl: options?.notaryUrl,
websocketProxyUrl: options?.websocketProxyUrl,
},
options?.secretHeaders || [],
options?.secretResps || [],
);
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);
return JSON.parse(raw);
}
}

View File

@@ -1,98 +1,22 @@
export interface Proof {
session: Session;
substrings: Substrings;
notaryUrl: string;
}
export interface Session {
header: Header;
signature: Signature;
session_info: SessionInfo;
}
export interface SessionInfo {
server_name: ServerName;
handshake_decommitment: HandshakeDecommitment;
}
export interface HandshakeDecommitment {
nonce: number[];
data: Data;
}
export interface Data {
server_cert_details: ServerCERTDetails;
server_kx_details: ServerKxDetails;
client_random: number[];
server_random: number[];
}
export interface ServerCERTDetails {
cert_chain: Array<number[]>;
ocsp_response: number[];
scts: null;
}
export interface ServerKxDetails {
kx_params: number[];
kx_sig: KxSig;
}
export interface KxSig {
scheme: string;
sig: number[];
}
export interface Header {
encoder_seed: number[];
merkle_root: number[];
sent_len: number;
recv_len: number;
handshake_summary: HandshakeSummary;
}
export interface HandshakeSummary {
time: number;
server_public_key: ServerPublicKey;
handshake_commitment: number[];
}
export interface ServerPublicKey {
group: string;
key: number[];
}
export interface ServerName {
Dns: string;
}
export interface Signature {
P256: string;
}
export interface Substrings {
openings: { [key: string]: Opening[] };
inclusion_proof: InclusionProof;
}
export interface InclusionProof {
proof: any[];
total_leaves: number;
}
export interface Opening {
kind?: string;
ranges?: Range[];
direction?: string;
Blake3?: Blake3;
}
export interface Blake3 {
data: number[];
nonce: number[];
}
export interface Range {
export type CommitData = {
start: number;
end: number;
}
};
export type ParsedTranscriptData = {
all: CommitData;
info: CommitData;
headers: { [key: string]: CommitData };
body?: CommitData;
json?: { [path: string]: CommitData };
lineBreaks: CommitData[];
};
export type ProofData = {
time: number;
server_dns: string;
sent: string;
sent_auth_ranges: { start: number; end: number }[];
recv: string;
recv_auth_ranges: { start: number; end: number }[];
};

408
src/utils.ts Normal file
View File

@@ -0,0 +1,408 @@
import { ParsedTranscriptData } from './types';
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);
}
export function stringToBuffer(str: string): number[] {
return Buffer.from(str).toJSON().data;
}
export function arrayToHex(uintArr: Uint8Array): string {
return Buffer.from(uintArr).toString('hex');
}

View File

@@ -1,6 +0,0 @@
import * as Comlink from 'comlink';
import TLSN from './tlsn';
export default TLSN;
Comlink.expose(TLSN);

View File

@@ -1,39 +1,80 @@
import { prove, verify } from '../../src';
import {
Prover as _Prover,
NotaryServer,
NotarizedSession as _NotarizedSession,
TlsProof as _TlsProof,
} from '../../src/lib';
import { assert } from '../utils';
import * as Comlink from 'comlink';
const { init, Prover, NotarizedSession, TlsProof }: 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 proof = await prove('https://swapi.dev/api/people/1', {
method: 'GET',
headers: { secret: 'test_secret' },
maxTranscriptSize: 16384,
notaryUrl: process.env.LOCAL_NOTARY
? 'http://localhost:7047'
: 'https://notary.pse.dev/v0.1.0-alpha.6',
websocketProxyUrl: process.env.LOCAL_WS
? 'ws://localhost:55688'
: 'wss://notary.pse.dev/proxy?token=swapi.dev',
secretHeaders: ['test_secret'],
secretResps: ['blond', 'fair'],
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 sessionHex = await prover.notarize(commit);
const session = (await new NotarizedSession(
sessionHex,
)) as _NotarizedSession;
const proofHex = await session.proof(commit);
console.log('proof:', proofHex);
const proof = (await new TlsProof(proofHex)) as _TlsProof;
console.timeEnd('prove');
console.log('Proof: ', JSON.stringify(proof));
console.time('verify');
const result = await verify(proof);
const result = await proof.verify({
typ: 'P256',
key: await notary.publicKey(),
});
console.timeEnd('verify');
console.log(result);
assert(result.sent.includes('host: swapi.dev'));
assert(result.sent.includes('secret: XXXXXXXXXXX'));
assert(result.recv.includes('Luke Skywalker'));
assert(result.recv.includes('"hair_color":"XXXXX"'));
assert(result.recv.includes('"skin_color":"XXXX"'));
assert(!result.sent.includes('secret: test_secret'));
assert(result.recv.includes('"name":"Luke Skywalker"'));
assert(result.recv.includes('"hair_color":"blond"'));
assert(result.recv.includes('"skin_color":"fair"'));
// @ts-ignore
document.getElementById('full-integration-swapi').textContent = 'OK';

File diff suppressed because one or more lines are too long

View File

@@ -46,8 +46,8 @@ const spawnTlsnServerFixture = () => {
let localNotaryServer: ChildProcess;
const spawnLocalNotaryServer = async () => {
const localNotaryServerPath = './utils/tlsn/notary-server/';
localNotaryServer = exec(`target/release/notary-server`, {
const localNotaryServerPath = './utils/tlsn/notary/server';
localNotaryServer = exec(`../target/release/notary-server`, {
cwd: localNotaryServerPath,
});
localNotaryServer.stdout?.on('data', (data) => {
@@ -74,7 +74,7 @@ const spawnLocalNotaryServer = async () => {
const configureNotarySerer = () => {
try {
const configPath = './utils/tlsn/notary-server/config/config.yaml';
const configPath = './utils/tlsn/notary/server/config/config.yaml';
const fileContents = fs.readFileSync(configPath, 'utf8');
const data = yaml.load(fileContents) as any;
data.tls.enabled = false;

9
test/worker.ts Normal file
View File

@@ -0,0 +1,9 @@
import * as Comlink from 'comlink';
import init, { Prover, NotarizedSession, TlsProof } from '../src/lib';
Comlink.expose({
init,
Prover,
NotarizedSession,
TlsProof,
});

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"module": "esnext",
"target": "es5",
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -22,7 +22,7 @@ fi
# Checkout the specific tag
git checkout "v0.1.0-alpha.6"
for dir in "tlsn/tlsn-server-fixture/" "notary-server"; do
for dir in "tlsn/tlsn-server-fixture/" "notary/server"; do
# Change to the specific subdirectory
cd ${dir}

View File

@@ -1,8 +0,0 @@
[target.wasm32-unknown-unknown]
rustflags = [
"-C",
"target-feature=+atomics,+bulk-memory,+mutable-globals",
]
[unstable]
build-std = ["panic_abort", "std"]

13
wasm/pkg/README.md Normal file
View File

@@ -0,0 +1,13 @@
# TLSNotary WASM bindings
## Build
This crate must be built using the nightly rust compiler with the following flags:
```bash
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \
rustup run nightly \
wasm-pack build --target web . -- -Zbuild-std=panic_abort,std
```

15
wasm/pkg/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "tlsn-wasm",
"type": "module",
"version": "0.1.0-alpha.6",
"files": [
"tlsn_wasm_bg.wasm",
"tlsn_wasm.js",
"tlsn_wasm.d.ts"
],
"main": "tlsn_wasm.js",
"types": "tlsn_wasm.d.ts",
"sideEffects": [
"./snippets/*"
]
}

View File

@@ -0,0 +1,6 @@
onmessage = function (ev) {
let [ia, index, value] = ev.data;
ia = new Int32Array(ia.buffer);
let result = Atomics.wait(ia, index, value);
postMessage(result);
};

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2022 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Note: this is never used, but necessary to prevent a bug in Firefox
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1702191) where it collects
// Web Workers that have a shared WebAssembly memory with the main thread,
// but are not explicitly rooted via a `Worker` instance.
//
// By storing them in a variable, we can keep `Worker` objects around and
// prevent them from getting GC-d.
let _workers;
export async function startWorkers(module, memory, builder) {
if (builder.numThreads() === 0) {
throw new Error(`num_threads must be > 0.`);
}
const workerInit = {
module,
memory,
receiver: builder.receiver()
};
_workers = await Promise.all(
Array.from({ length: builder.numThreads() }, async () => {
// Self-spawn into a new Worker.
//
// TODO: while `new URL('...', import.meta.url) becomes a semi-standard
// way to get asset URLs relative to the module across various bundlers
// and browser, ideally we should switch to `import.meta.resolve`
// once it becomes a standard.
const worker = new Worker(
new URL('./workerHelpers.worker.js', import.meta.url),
{
type: 'module'
}
);
worker.postMessage(workerInit);
await new Promise(resolve =>
worker.addEventListener('message', resolve, { once: true })
);
return worker;
})
);
builder.build();
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2022 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Note: our JS should have been generated in
// `[out-dir]/snippets/wasm-bindgen-rayon-[hash]/workerHelpers.worker.js`,
// resolve the main module via `../../..`.
//
// This might need updating if the generated structure changes on wasm-bindgen
// side ever in the future, but works well with bundlers today. The whole
// point of this crate, after all, is to abstract away unstable features
// and temporary bugs so that you don't need to deal with them in your code.
import initWbg, { wbg_rayon_start_worker } from '../../../tlsn_wasm.js';
onmessage = async ({ data: { module, memory, receiver } }) => {
await initWbg(module, memory);
postMessage(true);
wbg_rayon_start_worker(receiver);
};

294
wasm/pkg/tlsn_wasm.d.ts vendored Normal file
View File

@@ -0,0 +1,294 @@
/* tslint:disable */
/* eslint-disable */
/**
* Initializes logging.
* @param {LoggingConfig | undefined} [config]
*/
export function init_logging(config?: LoggingConfig): void;
/**
* @param {number} num_threads
* @returns {Promise<any>}
*/
export function initThreadPool(num_threads: number): Promise<any>;
/**
* @param {number} receiver
*/
export function wbg_rayon_start_worker(receiver: number): void;
export type Body = JsonValue;
export type Method = "GET" | "POST" | "PUT" | "DELETE";
export interface HttpRequest {
uri: string;
method: Method;
headers: Map<string, number[]>;
body: Body | undefined;
}
export interface HttpResponse {
status: number;
headers: [string, number[]][];
}
export interface Transcript {
sent: number[];
recv: number[];
}
export interface Commit {
sent: { start: number; end: number }[];
recv: { start: number; end: number }[];
}
export interface Reveal {
sent: { start: number; end: number }[];
recv: { start: number; end: number }[];
}
export type KeyType = "P256";
export interface NotaryPublicKey {
typ: KeyType;
key: string;
}
export interface ProofData {
time: number;
server_dns: string;
sent: number[];
sent_auth_ranges: { start: number; end: number }[];
received: number[];
received_auth_ranges: { start: number; end: number }[];
}
export interface VerifierData {
server_dns: string;
sent: number[];
sent_auth_ranges: { start: number; end: number }[];
received: number[];
received_auth_ranges: { start: number; end: number }[];
}
export interface CrateLogFilter {
level: LoggingLevel;
name: string;
}
export interface LoggingConfig {
level: LoggingLevel | undefined;
crate_filters: CrateLogFilter[] | undefined;
span_events: SpanEvent[] | undefined;
}
export type SpanEvent = "New" | "Close" | "Active";
export type LoggingLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error";
export interface VerifierConfig {
id: string;
max_sent_data: number | undefined;
max_received_data: number | undefined;
}
export interface ProverConfig {
id: string;
server_dns: string;
max_sent_data: number | undefined;
max_recv_data: number | undefined;
}
/**
*/
export class NotarizedSession {
free(): void;
/**
* Builds a new proof.
* @param {Reveal} reveal
* @returns {TlsProof}
*/
proof(reveal: Reveal): TlsProof;
/**
* Returns the transcript.
* @returns {Transcript}
*/
transcript(): Transcript;
/**
* Serializes to a byte array.
* @returns {Uint8Array}
*/
serialize(): Uint8Array;
/**
* Deserializes from a byte array.
* @param {Uint8Array} bytes
* @returns {NotarizedSession}
*/
static deserialize(bytes: Uint8Array): NotarizedSession;
}
/**
*/
export class Prover {
free(): void;
/**
* @param {ProverConfig} config
*/
constructor(config: ProverConfig);
/**
* Set up the prover.
*
* This performs all MPC setup prior to establishing the connection to the
* application server.
* @param {string} verifier_url
* @returns {Promise<void>}
*/
setup(verifier_url: string): Promise<void>;
/**
* Send the HTTP request to the server.
* @param {string} ws_proxy_url
* @param {HttpRequest} request
* @returns {Promise<HttpResponse>}
*/
send_request(ws_proxy_url: string, request: HttpRequest): Promise<HttpResponse>;
/**
* Returns the transcript.
* @returns {Transcript}
*/
transcript(): Transcript;
/**
* Runs the notarization protocol.
* @param {Commit} commit
* @returns {Promise<NotarizedSession>}
*/
notarize(commit: Commit): Promise<NotarizedSession>;
/**
* Reveals data to the verifier and finalizes the protocol.
* @param {Reveal} reveal
* @returns {Promise<void>}
*/
reveal(reveal: Reveal): Promise<void>;
}
/**
*/
export class TlsProof {
free(): void;
/**
* @returns {Uint8Array}
*/
serialize(): Uint8Array;
/**
* @param {Uint8Array} bytes
* @returns {TlsProof}
*/
static deserialize(bytes: Uint8Array): TlsProof;
/**
* Verifies the proof using the provided notary public key.
* @param {NotaryPublicKey} notary_key
* @returns {ProofData}
*/
verify(notary_key: NotaryPublicKey): ProofData;
}
/**
*/
export class Verifier {
free(): void;
/**
* @param {VerifierConfig} config
*/
constructor(config: VerifierConfig);
/**
* Connect to the prover.
* @param {string} prover_url
* @returns {Promise<void>}
*/
connect(prover_url: string): Promise<void>;
/**
* Verifies the connection and finalizes the protocol.
* @returns {Promise<VerifierData>}
*/
verify(): Promise<VerifierData>;
}
/**
*/
export class wbg_rayon_PoolBuilder {
free(): void;
/**
* @returns {number}
*/
numThreads(): number;
/**
* @returns {number}
*/
receiver(): number;
/**
*/
build(): void;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly __wbg_notarizedsession_free: (a: number) => void;
readonly notarizedsession_proof: (a: number, b: number, c: number) => void;
readonly notarizedsession_transcript: (a: number) => number;
readonly notarizedsession_serialize: (a: number, b: number) => void;
readonly notarizedsession_deserialize: (a: number, b: number, c: number) => void;
readonly __wbg_tlsproof_free: (a: number) => void;
readonly tlsproof_serialize: (a: number, b: number) => void;
readonly tlsproof_deserialize: (a: number, b: number, c: number) => void;
readonly tlsproof_verify: (a: number, b: number, c: number) => void;
readonly __wbg_verifier_free: (a: number) => void;
readonly verifier_new: (a: number) => number;
readonly verifier_connect: (a: number, b: number, c: number) => number;
readonly verifier_verify: (a: number) => number;
readonly init_logging: (a: number) => void;
readonly __wbg_prover_free: (a: number) => void;
readonly prover_new: (a: number) => number;
readonly prover_setup: (a: number, b: number, c: number) => number;
readonly prover_send_request: (a: number, b: number, c: number, d: number) => number;
readonly prover_transcript: (a: number, b: number) => void;
readonly prover_notarize: (a: number, b: number) => number;
readonly prover_reveal: (a: number, b: number) => number;
readonly __wbg_wbg_rayon_poolbuilder_free: (a: number) => void;
readonly wbg_rayon_poolbuilder_numThreads: (a: number) => number;
readonly wbg_rayon_poolbuilder_receiver: (a: number) => number;
readonly wbg_rayon_poolbuilder_build: (a: number) => void;
readonly initThreadPool: (a: number) => number;
readonly wbg_rayon_start_worker: (a: number) => void;
readonly ring_core_0_17_8_bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
readonly memory: WebAssembly.Memory;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export_3: WebAssembly.Table;
readonly _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h55b2cafb95688ebd: (a: number, b: number) => void;
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd2e6f08741139974: (a: number, b: number, c: number) => void;
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6f377bea5980efdf: (a: number, b: number, c: number) => void;
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h71d6551dc02f3cc7: (a: number, b: number, c: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_exn_store: (a: number) => void;
readonly wasm_bindgen__convert__closures__invoke2_mut__h0a86b19f1fa78a2d: (a: number, b: number, c: number, d: number) => void;
readonly __wbindgen_thread_destroy: (a?: number, b?: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {SyncInitInput} module
* @param {WebAssembly.Memory} maybe_memory
*
* @returns {InitOutput}
*/
export function initSync(module: SyncInitInput, maybe_memory?: WebAssembly.Memory): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
* @param {WebAssembly.Memory} maybe_memory
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: InitInput | Promise<InitInput>, maybe_memory?: WebAssembly.Memory): Promise<InitOutput>;

1315
wasm/pkg/tlsn_wasm.js Normal file

File diff suppressed because it is too large Load Diff

BIN
wasm/pkg/tlsn_wasm_bg.wasm Normal file

Binary file not shown.

44
wasm/pkg/tlsn_wasm_bg.wasm.d.ts vendored Normal file
View File

@@ -0,0 +1,44 @@
/* tslint:disable */
/* eslint-disable */
export function __wbg_notarizedsession_free(a: number): void;
export function notarizedsession_proof(a: number, b: number, c: number): void;
export function notarizedsession_transcript(a: number): number;
export function notarizedsession_serialize(a: number, b: number): void;
export function notarizedsession_deserialize(a: number, b: number, c: number): void;
export function __wbg_tlsproof_free(a: number): void;
export function tlsproof_serialize(a: number, b: number): void;
export function tlsproof_deserialize(a: number, b: number, c: number): void;
export function tlsproof_verify(a: number, b: number, c: number): void;
export function __wbg_verifier_free(a: number): void;
export function verifier_new(a: number): number;
export function verifier_connect(a: number, b: number, c: number): number;
export function verifier_verify(a: number): number;
export function init_logging(a: number): void;
export function __wbg_prover_free(a: number): void;
export function prover_new(a: number): number;
export function prover_setup(a: number, b: number, c: number): number;
export function prover_send_request(a: number, b: number, c: number, d: number): number;
export function prover_transcript(a: number, b: number): void;
export function prover_notarize(a: number, b: number): number;
export function prover_reveal(a: number, b: number): number;
export function __wbg_wbg_rayon_poolbuilder_free(a: number): void;
export function wbg_rayon_poolbuilder_numThreads(a: number): number;
export function wbg_rayon_poolbuilder_receiver(a: number): number;
export function wbg_rayon_poolbuilder_build(a: number): void;
export function initThreadPool(a: number): number;
export function wbg_rayon_start_worker(a: number): void;
export function ring_core_0_17_8_bn_mul_mont(a: number, b: number, c: number, d: number, e: number, f: number): void;
export const memory: WebAssembly.Memory;
export function __wbindgen_malloc(a: number, b: number): number;
export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number;
export const __wbindgen_export_3: WebAssembly.Table;
export function _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h55b2cafb95688ebd(a: number, b: number): void;
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd2e6f08741139974(a: number, b: number, c: number): void;
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6f377bea5980efdf(a: number, b: number, c: number): void;
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h71d6551dc02f3cc7(a: number, b: number, c: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number, c: number): void;
export function __wbindgen_exn_store(a: number): void;
export function wasm_bindgen__convert__closures__invoke2_mut__h0a86b19f1fa78a2d(a: number, b: number, c: number, d: number): void;
export function __wbindgen_thread_destroy(a: number, b: number): void;
export function __wbindgen_start(): void;

View File

@@ -1,91 +0,0 @@
[package]
authors = ["The tlsn-extension Developers"]
description = "tlsn-js library for using TLSNotary in browsers"
edition = "2018"
license = "MIT OR Apache-2.0"
name = "tlsn-extension-rs"
rust-version = "1.56"
version = "0.1.0"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
chrono = "0.4"
elliptic-curve = { version = "0.13.5", features = ["pkcs8"] }
futures = "0.3"
futures-util = "0.3.28"
getrandom = { version = "0.2", features = ["js"] }
js-sys = "0.3.64"
p256 = { version = "0.13", features = ["pem", "ecdsa"] }
rayon = "1.5"
serde = { version = "1.0.147", features = ["derive"] }
serde-wasm-bindgen = "0.6.1"
serde_json = "1.0"
tracing = "0.1"
url = { version = "2.0", features = ["serde"] }
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
wasm-bindgen-rayon = "1.0"
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"] }
tracing-subscriber = { version = "0.3", features = ["time","env-filter"] }
tracing-web = "0.1.2"
ring = { version = "0.17", features = ["wasm32_unknown_unknown_js"] }
# time crate: https://crates.io/crates/time
# NOTE: It is required, otherwise "time not implemented on this platform" error happens right after "!@# 2".
# Probably due to tokio's time feature is used in tlsn-prover?
time = { version = "0.3.34", features = ["wasm-bindgen"] }
# Used to calculate elapsed time.
web-time = "1.0"
tlsn-core = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.6", package = "tlsn-core" }
tlsn-prover = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.6", package = "tlsn-prover" }
web-sys = { version = "0.3.4", features = [
"BinaryType",
"Blob",
"ErrorEvent",
"FileReader",
"MessageEvent",
"ProgressEvent",
"WebSocket",
"console",
'Document',
'HtmlElement',
'HtmlInputElement',
'Window',
'Worker',
'Headers',
'Request',
'RequestInit',
'RequestMode',
'Response',
] }
# Use the patched ws_stream_wasm to fix the issue https://github.com/najamelan/ws_stream_wasm/issues/12#issuecomment-1711902958
ws_stream_wasm = { version = "0.7.4", git = "https://github.com/tlsnotary/ws_stream_wasm", rev = "2ed12aad9f0236e5321f577672f309920b2aef51" }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7" }
strum = { version = "0.26.1" }
strum_macros = "0.26.1"
[dev-dependencies]
wasm-bindgen-test = "0.3.34"
[profile.release]
lto = true # Enable Link Time Optimization
opt-level = "z" # Optimize for size
[package.metadata.wasm-pack.profile.release]
wasm-opt = true

View File

@@ -1,88 +0,0 @@
use core::slice;
use std::pin::Pin;
use std::task::{Context, Poll};
use pin_project_lite::pin_project;
pin_project! {
#[derive(Debug)]
pub(crate) struct FuturesIo<T> {
#[pin]
inner: T,
}
}
impl<T> FuturesIo<T> {
/// Create a new `FuturesIo` wrapping the given I/O object.
///
/// # Safety
///
/// This wrapper is only safe to use if the inner I/O object does not under any circumstance
/// read from the buffer passed to `poll_read` in the `futures::AsyncRead` implementation.
pub(crate) unsafe fn new(inner: T) -> Self {
Self { inner }
}
pub(crate) fn into_inner(self) -> T {
self.inner
}
}
impl<T> hyper::rt::Write for FuturesIo<T>
where
T: futures::AsyncWrite + Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
self.project().inner.poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
self.project().inner.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
self.project().inner.poll_close(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>> {
self.project().inner.poll_write_vectored(cx, bufs)
}
}
// Adapted from https://github.com/hyperium/hyper-util/blob/99b77a5a6f75f24bc0bcb4ca74b5f26a07b19c80/src/rt/tokio.rs
impl<T> hyper::rt::Read for FuturesIo<T>
where
T: futures::AsyncRead + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: hyper::rt::ReadBufCursor<'_>,
) -> Poll<Result<(), std::io::Error>> {
// Safety: buf_slice should only be written to, so it's safe to convert `&mut [MaybeUninit<u8>]` to `&mut [u8]`.
let buf_slice = unsafe {
slice::from_raw_parts_mut(buf.as_mut().as_mut_ptr() as *mut u8, buf.as_mut().len())
};
let n = match futures::AsyncRead::poll_read(self.project().inner, cx, buf_slice) {
Poll::Ready(Ok(n)) => n,
other => return other.map_ok(|_| ()),
};
unsafe {
buf.advance(n);
}
Poll::Ready(Ok(()))
}
}

View File

@@ -1,74 +0,0 @@
pub(crate) mod hyper_io;
mod request_opt;
mod requests;
pub mod prover;
pub use prover::prover;
pub mod verify;
use tracing::error;
pub use verify::verify;
use wasm_bindgen::prelude::*;
pub use crate::request_opt::{RequestOptions, VerifyResult};
pub use wasm_bindgen_rayon::init_thread_pool;
use js_sys::JSON;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, Response};
use std::panic;
use tracing::debug;
use tracing_subscriber::fmt::format::Pretty;
use tracing_subscriber::fmt::time::UtcTime;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
use tracing_web::{performance_layer, MakeWebConsoleWriter};
extern crate console_error_panic_hook;
#[wasm_bindgen]
pub fn setup_tracing_web(logging_filter: &str) {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false) // Only partially supported across browsers
.with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers
// .with_thread_ids(true)
// .with_thread_names(true)
.with_writer(MakeWebConsoleWriter::new()); // write events to the console
let perf_layer = performance_layer().with_details_from_fields(Pretty::default());
let filter_layer = EnvFilter::builder()
.parse(logging_filter)
.unwrap_or_default();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(perf_layer)
.init(); // Install these as subscribers to tracing events
// https://github.com/rustwasm/console_error_panic_hook
panic::set_hook(Box::new(|info| {
error!("panic occurred: {:?}", info);
console_error_panic_hook::hook(info);
}));
debug!("🪵 Logging set up 🪵")
}
pub async fn fetch_as_json_string(url: &str, opts: &RequestInit) -> Result<String, JsValue> {
let request = Request::new_with_str_and_init(url, opts)?;
let window = web_sys::window().expect("Window object");
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
assert!(resp_value.is_instance_of::<Response>());
let resp: Response = resp_value.dyn_into()?;
let json = JsFuture::from(resp.json()?).await?;
let stringified = JSON::stringify(&json)?;
stringified
.as_string()
.ok_or_else(|| JsValue::from_str("Could not stringify JSON"))
}

View File

@@ -1,479 +0,0 @@
use futures::channel::oneshot;
use std::ops::Range;
use tlsn_prover::tls::{Prover, ProverConfig};
use wasm_bindgen_futures::spawn_local;
use web_time::Instant;
use ws_stream_wasm::*;
use crate::hyper_io::FuturesIo;
use crate::request_opt::RequestOptions;
use crate::requests::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse};
pub use wasm_bindgen_rayon::init_thread_pool;
use crate::fetch_as_json_string;
pub use crate::request_opt::VerifyResult;
use futures::AsyncWriteExt;
use http_body_util::{BodyExt, Full};
use hyper::{body::Bytes, Request, StatusCode};
use js_sys::Array;
use strum::EnumMessage;
use tlsn_core::proof::TlsProof;
use url::Url;
use wasm_bindgen::prelude::*;
use web_sys::{Headers, RequestInit, RequestMode};
use tracing::{debug, info, trace};
#[derive(strum_macros::EnumMessage, Debug, Clone, Copy)]
#[allow(dead_code)]
enum ProverPhases {
#[strum(message = "Connect application server with websocket proxy")]
ConnectWsProxy,
#[strum(message = "Build prover config")]
BuildProverConfig,
#[strum(message = "Set up prover")]
SetUpProver,
#[strum(message = "Bind the prover to the server connection")]
BindProverToConnection,
#[strum(message = "Spawn the prover thread")]
SpawnProverThread,
#[strum(message = "Attach the hyper HTTP client to the TLS connection")]
AttachHttpClient,
#[strum(message = "Spawn the HTTP task to be run concurrently")]
SpawnHttpTask,
#[strum(message = "Build request")]
BuildRequest,
#[strum(message = "Start MPC-TLS connection with the server")]
StartMpcConnection,
#[strum(message = "Received response from the server")]
ReceivedResponse,
#[strum(message = "Parsing response from the server")]
ParseResponse,
#[strum(message = "Close the connection to the server")]
CloseConnection,
#[strum(message = "Start notarization")]
StartNotarization,
#[strum(message = "Commit to data")]
Commit,
#[strum(message = "Finalize")]
Finalize,
#[strum(message = "Notarization complete")]
NotarizationComplete,
#[strum(message = "Create Proof")]
CreateProof,
}
fn log_phase(phase: ProverPhases) {
info!("tlsn-js {}: {}", phase as u8, phase.get_message().unwrap());
}
#[wasm_bindgen]
pub async fn prover(
target_url_str: &str,
val: JsValue,
secret_headers: JsValue,
secret_body: JsValue,
) -> Result<String, JsValue> {
debug!("target_url: {}", target_url_str);
let target_url = Url::parse(target_url_str)
.map_err(|e| JsValue::from_str(&format!("Could not parse target_url: {:?}", e)))?;
debug!(
"target_url.host: {}",
target_url
.host()
.ok_or(JsValue::from_str("Could not get target host"))?
);
let options: RequestOptions = serde_wasm_bindgen::from_value(val)
.map_err(|e| JsValue::from_str(&format!("Could not deserialize options: {:?}", e)))?;
debug!("options.notary_url: {}", options.notary_url.as_str());
let start_time = Instant::now();
/*
* Connect Notary with websocket
*/
let mut opts = RequestInit::new();
opts.method("POST");
// opts.method("GET");
opts.mode(RequestMode::Cors);
// set headers
let headers = Headers::new()
.map_err(|e| JsValue::from_str(&format!("Could not create headers: {:?}", e)))?;
let notary_url = Url::parse(options.notary_url.as_str())
.map_err(|e| JsValue::from_str(&format!("Could not parse notary_url: {:?}", e)))?;
let notary_ssl = notary_url.scheme() == "https" || notary_url.scheme() == "wss";
let notary_host = notary_url.authority();
let notary_path = notary_url.path();
let notary_path_str = if notary_path == "/" { "" } else { notary_path };
headers
.append("Host", notary_host)
.map_err(|e| JsValue::from_str(&format!("Could not append Host header: {:?}", e)))?;
headers
.append("Content-Type", "application/json")
.map_err(|e| {
JsValue::from_str(&format!("Could not append Content-Type header: {:?}", e))
})?;
opts.headers(&headers);
info!("notary_host: {}", notary_host);
// set body
let payload = serde_json::to_string(&NotarizationSessionRequest {
client_type: ClientType::Websocket,
max_sent_data: options.max_sent_data,
max_recv_data: options.max_recv_data,
})
.map_err(|e| JsValue::from_str(&format!("Could not serialize request: {:?}", e)))?;
opts.body(Some(&JsValue::from_str(&payload)));
// url
let url = format!(
"{}://{}{}/session",
if notary_ssl { "https" } else { "http" },
notary_host,
notary_path_str
);
debug!("Request: {}", url);
let rust_string = fetch_as_json_string(&url, &opts)
.await
.map_err(|e| JsValue::from_str(&format!("Could not fetch session: {:?}", e)))?;
let notarization_response =
serde_json::from_str::<NotarizationSessionResponse>(&rust_string)
.map_err(|e| JsValue::from_str(&format!("Could not deserialize response: {:?}", e)))?;
debug!("Response: {}", rust_string);
debug!("Notarization response: {:?}", notarization_response,);
let notary_wss_url = format!(
"{}://{}{}/notarize?sessionId={}",
if notary_ssl { "wss" } else { "ws" },
notary_host,
notary_path_str,
notarization_response.session_id
);
let (_, notary_ws_stream) = WsMeta::connect(notary_wss_url, None)
.await
.expect_throw("assume the notary ws connection succeeds");
let notary_ws_stream_into = notary_ws_stream.into_io();
log_phase(ProverPhases::BuildProverConfig);
let target_host = target_url
.host_str()
.ok_or(JsValue::from_str("Could not get target host"))?;
// Basic default prover config
let mut builder = ProverConfig::builder();
if let Some(max_sent_data) = options.max_sent_data {
builder.max_sent_data(max_sent_data);
}
if let Some(max_recv_data) = options.max_recv_data {
builder.max_recv_data(max_recv_data);
}
let config = builder
.id(notarization_response.session_id)
.server_dns(target_host)
.build()
.map_err(|e| JsValue::from_str(&format!("Could not build prover config: {:?}", e)))?;
// Create a Prover and set it up with the Notary
// This will set up the MPC backend prior to connecting to the server.
log_phase(ProverPhases::SetUpProver);
let prover = Prover::new(config)
.setup(notary_ws_stream_into)
.await
.map_err(|e| JsValue::from_str(&format!("Could not set up prover: {:?}", e)))?;
/*
Connect Application Server with websocket proxy
*/
log_phase(ProverPhases::ConnectWsProxy);
let (_, client_ws_stream) = WsMeta::connect(options.websocket_proxy_url, None)
.await
.expect_throw("assume the client ws connection succeeds");
// Bind the Prover to the server connection.
// The returned `mpc_tls_connection` is an MPC TLS connection to the Server: all data written
// to/read from it will be encrypted/decrypted using MPC with the Notary.
log_phase(ProverPhases::BindProverToConnection);
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_ctrl = prover_fut.control();
log_phase(ProverPhases::SpawnProverThread);
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
log_phase(ProverPhases::AttachHttpClient);
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
log_phase(ProverPhases::SpawnHttpTask);
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);
log_phase(ProverPhases::BuildRequest);
let mut req_with_header = Request::builder()
.uri(target_url_str)
.method(options.method.as_str());
for (key, value) in options.headers {
info!("adding header: {} - {}", key.as_str(), value.as_str());
req_with_header = req_with_header.header(key.as_str(), value.as_str());
}
let req_with_body = if options.body.is_empty() {
info!("empty body");
req_with_header.body(Full::new(Bytes::default()))
} else {
info!("added body - {}", options.body.as_str());
req_with_header.body(Full::from(options.body))
};
let unwrapped_request = req_with_body
.map_err(|e| JsValue::from_str(&format!("Could not build request: {:?}", e)))?;
log_phase(ProverPhases::StartMpcConnection);
// Defer decryption of the response.
prover_ctrl
.defer_decryption()
.await
.map_err(|e| JsValue::from_str(&format!("failed to enable deferred decryption: {}", e)))?;
// Send the request to the Server and get a response via the MPC TLS connection
let response = request_sender
.send_request(unwrapped_request)
.await
.map_err(|e| JsValue::from_str(&format!("Could not send request: {:?}", e)))?;
log_phase(ProverPhases::ReceivedResponse);
if response.status() != StatusCode::OK {
return Err(JsValue::from_str(&format!(
"Response status is not OK: {:?}",
response.status()
)));
}
log_phase(ProverPhases::ParseResponse);
// Pretty printing :)
let payload = response
.into_body()
.collect()
.await
.map_err(|e| JsValue::from_str(&format!("Could not get response body: {:?}", e)))?
.to_bytes();
let parsed = serde_json::from_str::<serde_json::Value>(&String::from_utf8_lossy(&payload))
.map_err(|e| JsValue::from_str(&format!("Could not parse response: {:?}", e)))?;
let response_pretty = serde_json::to_string_pretty(&parsed)
.map_err(|e| JsValue::from_str(&format!("Could not serialize response: {:?}", e)))?;
info!("Response: {}", response_pretty);
// Close the connection to the server
log_phase(ProverPhases::CloseConnection);
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)))?;
// The Prover task should be done now, so we can grab it.
log_phase(ProverPhases::StartNotarization);
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_notarize();
let secret_headers_vecs = string_list_to_bytes_vec(&secret_headers)?;
let secret_headers_slices: Vec<&[u8]> = secret_headers_vecs
.iter()
.map(|vec| vec.as_slice())
.collect();
// Identify the ranges in the transcript that contain revealed_headers
let (sent_public_ranges, sent_private_ranges) = find_ranges(
prover.sent_transcript().data(),
secret_headers_slices.as_slice(),
);
let secret_body_vecs = string_list_to_bytes_vec(&secret_body)?;
let secret_body_slices: Vec<&[u8]> =
secret_body_vecs.iter().map(|vec| vec.as_slice()).collect();
// Identify the ranges in the transcript that contain the only data we want to reveal later
let (recv_public_ranges, recv_private_ranges) = find_ranges(
prover.recv_transcript().data(),
secret_body_slices.as_slice(),
);
log_phase(ProverPhases::Commit);
let _recv_len = prover.recv_transcript().data().len();
let builder = prover.commitment_builder();
// Commit to the outbound and inbound transcript, isolating the data that contain secrets
let sent_pub_commitment_ids = sent_public_ranges
.iter()
.map(|range| {
builder.commit_sent(range).map_err(|e| {
JsValue::from_str(&format!("Error committing sent pub range: {:?}", e))
})
})
.collect::<Result<Vec<_>, _>>()?;
sent_private_ranges.iter().try_for_each(|range| {
builder
.commit_sent(range)
.map_err(|e| {
JsValue::from_str(&format!("Error committing sent private range: {:?}", e))
})
.map(|_| ())
})?;
let recv_pub_commitment_ids = recv_public_ranges
.iter()
.map(|range| {
builder.commit_recv(range).map_err(|e| {
JsValue::from_str(&format!("Error committing recv public ranges: {:?}", e))
})
})
.collect::<Result<Vec<_>, _>>()?;
recv_private_ranges.iter().try_for_each(|range| {
builder
.commit_recv(range)
.map_err(|e| {
JsValue::from_str(&format!("Error committing recv private range: {:?}", e))
})
.map(|_| ())
})?;
// Finalize, returning the notarized session
log_phase(ProverPhases::Finalize);
let notarized_session = prover
.finalize()
.await
.map_err(|e| JsValue::from_str(&format!("Error finalizing prover: {:?}", e)))?;
log_phase(ProverPhases::NotarizationComplete);
// Create a proof for all committed data in this session
log_phase(ProverPhases::CreateProof);
let session_proof = notarized_session.session_proof();
let mut proof_builder = notarized_session.data().build_substrings_proof();
// Reveal everything except the redacted stuff (which for the response it's everything except the screen_name)
sent_pub_commitment_ids
.iter()
.chain(recv_pub_commitment_ids.iter())
.try_for_each(|id| {
proof_builder
.reveal_by_id(*id)
.map_err(|e| JsValue::from_str(&format!("Could not reveal commitment: {:?}", e)))
.map(|_| ())
})?;
let substrings_proof = proof_builder
.build()
.map_err(|e| JsValue::from_str(&format!("Could not build proof: {:?}", e)))?;
let proof = TlsProof {
session: session_proof,
substrings: substrings_proof,
};
let res = serde_json::to_string_pretty(&proof)
.map_err(|e| JsValue::from_str(&format!("Could not serialize proof: {:?}", e)))?;
let duration = start_time.elapsed();
info!("!@# request took {} seconds", duration.as_secs());
Ok(res)
}
/// Find the ranges of the public and private parts of a sequence.
///
/// Returns a tuple of `(public, private)` ranges.
fn find_ranges(seq: &[u8], private_seq: &[&[u8]]) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
let mut private_ranges = Vec::new();
for s in private_seq {
for (idx, w) in seq.windows(s.len()).enumerate() {
if w == *s {
private_ranges.push(idx..(idx + w.len()));
}
}
}
let mut sorted_ranges = private_ranges.clone();
sorted_ranges.sort_by_key(|r| r.start);
let mut public_ranges = Vec::new();
let mut last_end = 0;
for r in sorted_ranges {
if r.start > last_end {
public_ranges.push(last_end..r.start);
}
last_end = r.end;
}
if last_end < seq.len() {
public_ranges.push(last_end..seq.len());
}
(public_ranges, private_ranges)
}
fn string_list_to_bytes_vec(secrets: &JsValue) -> Result<Vec<Vec<u8>>, JsValue> {
let array: Array = Array::from(secrets);
let length = array.length();
let mut byte_slices: Vec<Vec<u8>> = Vec::new();
for i in 0..length {
let secret_js: JsValue = array.get(i);
let secret_str: String = secret_js
.as_string()
.ok_or(JsValue::from_str("Could not convert secret to string"))?;
let secret_bytes = secret_str.into_bytes();
byte_slices.push(secret_bytes);
}
Ok(byte_slices)
}

View File

@@ -1,30 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Requestion Options of Fetch API
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestOptions {
pub method: String, // *GET, POST, PUT, DELETE, etc.
// pub mode: String, // no-cors, *cors, same-origin
// pub cache: String, // *default, no-cache, reload, force-cache, only-if-cached
// pub credentials: String, // include, *same-origin, omit
pub headers: HashMap<String, String>,
// pub redirect: String, // manual, *follow, error
// pub referrer_policy: String, // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
pub body: String, // body data type must match "Content-Type" header
pub max_sent_data: Option<usize>,
pub max_recv_data: Option<usize>,
pub notary_url: String,
pub websocket_proxy_url: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyResult {
pub server_name: String,
pub time: u64,
pub sent: String,
pub recv: String,
}

View File

@@ -1,29 +0,0 @@
use serde::{Deserialize, Serialize};
/// Response object of the /session API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotarizationSessionResponse {
/// Unique session id that is generated by notary and shared to prover
pub session_id: String,
}
/// Request object of the /session API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotarizationSessionRequest {
pub client_type: ClientType,
/// Maximum number of bytes that can be sent in bytes.
pub max_sent_data: Option<usize>,
/// Maximum number of bytes that can be received in bytes.
pub max_recv_data: Option<usize>,
}
/// Types of client that the prover is using
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ClientType {
/// Client that has access to the transport layer
Tcp,
/// Client that cannot directly access transport layer, e.g. browser extension
Websocket,
}

View File

@@ -1,104 +0,0 @@
use tracing::info;
use wasm_bindgen::prelude::*;
use crate::request_opt::VerifyResult;
use elliptic_curve::pkcs8::DecodePublicKey;
use std::time::Duration;
use tlsn_core::proof::{SessionProof, TlsProof};
#[wasm_bindgen]
pub async fn verify(proof: &str, notary_pubkey_str: &str) -> Result<String, JsValue> {
let proof: TlsProof = serde_json::from_str(proof)
.map_err(|e| JsValue::from_str(&format!("Could not deserialize proof: {:?}", e)))?;
let TlsProof {
// The session proof establishes the identity of the server and the commitments
// to the TLS transcript.
session,
// The substrings proof proves select portions of the transcript, while redacting
// anything the Prover chose not to disclose.
substrings,
} = proof;
info!(
"!@# notary_pubkey {}, {}",
notary_pubkey_str,
notary_pubkey_str.len()
);
session
.verify_with_default_cert_verifier(get_notary_pubkey(notary_pubkey_str)?)
.map_err(|e| JsValue::from_str(&format!("Session verification failed: {:?}", e)))?;
let SessionProof {
// The session header that was signed by the Notary is a succinct commitment to the TLS transcript.
header,
// This is the server name, checked against the certificate chain shared in the TLS handshake.
session_info,
..
} = session;
// The time at which the session was recorded
let time = chrono::DateTime::UNIX_EPOCH + Duration::from_secs(header.time());
// Verify the substrings proof against the session header.
//
// This returns the redacted transcripts
let (mut sent, mut recv) = substrings
.verify(&header)
.map_err(|e| JsValue::from_str(&format!("Could not verify substrings: {:?}", e)))?;
// Replace the bytes which the Prover chose not to disclose with 'X'
sent.set_redacted(b'X');
recv.set_redacted(b'X');
info!("-------------------------------------------------------------------");
info!(
"Successfully verified that the bytes below came from a session with {:?} at {}.",
session_info.server_name, time
);
info!("Note that the bytes which the Prover chose not to disclose are shown as X.");
info!("Bytes sent:");
info!(
"{}",
String::from_utf8(sent.data().to_vec()).map_err(|e| JsValue::from_str(&format!(
"Could not convert sent data to string: {:?}",
e
)))?
);
info!("Bytes received:");
info!(
"{}",
String::from_utf8(recv.data().to_vec()).map_err(|e| JsValue::from_str(&format!(
"Could not convert recv data to string: {:?}",
e
)))?
);
info!("-------------------------------------------------------------------");
let result = VerifyResult {
server_name: String::from(session_info.server_name.as_str()),
time: header.time(),
sent: String::from_utf8(sent.data().to_vec()).map_err(|e| {
JsValue::from_str(&format!("Could not convert sent data to string: {:?}", e))
})?,
recv: String::from_utf8(recv.data().to_vec()).map_err(|e| {
JsValue::from_str(&format!("Could not convert recv data to string: {:?}", e))
})?,
};
let res = serde_json::to_string_pretty(&result)
.map_err(|e| JsValue::from_str(&format!("Could not serialize result: {:?}", e)))?;
Ok(res)
}
#[allow(unused)]
fn print_type_of<T: ?Sized>(_: &T) {
info!("{}", std::any::type_name::<T>());
}
/// Returns a Notary pubkey trusted by this Verifier
fn get_notary_pubkey(pubkey: &str) -> Result<p256::PublicKey, JsValue> {
p256::PublicKey::from_public_key_pem(pubkey)
.map_err(|e| JsValue::from_str(&format!("Could not get notary pubkey: {:?}", e)))
}

View File

@@ -1,61 +0,0 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use serde_json::Value;
use std::{collections::HashMap, str};
use wasm_bindgen_test::*;
use web_sys::RequestInit;
extern crate tlsn_extension_rs;
use tlsn_extension_rs::*;
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn test_fetch() {
let url = "https://swapi.info/api/";
let mut opts = RequestInit::new();
opts.method("GET");
let rust_string: String = tlsn_extension_rs::fetch_as_json_string(&url, &opts)
.await
.unwrap();
assert!(rust_string.contains("starships"));
}
#[wasm_bindgen_test]
async fn verify() {
let pem = str::from_utf8(include_bytes!("../../../test/assets/notary.pem")).unwrap();
let proof = str::from_utf8(include_bytes!(
"../../../test/assets/simple_proof_redacted.json"
))
.unwrap();
let m: HashMap<String, Value> = serde_json::from_str(
&str::from_utf8(include_bytes!(
"../../../test/assets/simple_proof_expected.json"
))
.unwrap(),
)
.unwrap();
let result = tlsn_extension_rs::verify(proof, pem).await.expect("result");
log!("result: {}", &result);
let r: VerifyResult = serde_json::from_str::<VerifyResult>(&result).unwrap();
assert_eq!(r.server_name, m["serverName"]);
assert!(r.recv.contains("<title>XXXXXXXXXXXXXX</title>"));
assert_eq!(r.time, m["time"].as_u64().unwrap());
assert_eq!(r.sent, m["sent"].as_str().unwrap());
assert_eq!(r.recv, m["recv"].as_str().unwrap());
}

View File

@@ -1,20 +0,0 @@
{
"moz:firefoxOptions": {
"prefs": {
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true
},
"args": []
},
"goog:chromeOptions": {
"args": [
"--use-fake-device-for-media-stream",
"--use-fake-ui-for-media-stream",
"--headless",
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage",
"--window-size=1280,800"
]
}
}

View File

@@ -30,9 +30,9 @@ module.exports = [
{
mode: isProd ? 'production' : 'development',
entry: {
index: path.join(__dirname, 'src', 'index.ts'),
lib: path.join(__dirname, 'src', 'lib.ts'),
},
target: 'web',
target: 'webworker',
devtool: 'source-map',
resolve: {
extensions: ['.ts', '.js'],

View File

@@ -1,7 +1,6 @@
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const { compilerOptions } = require('./tsconfig.json');
const isProd = process.env.NODE_ENV === 'production';
@@ -49,25 +48,25 @@ 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,
},
// 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],