feat: return raw data from transcript and move parsing to client side (#92)

This commit is contained in:
tsukino
2025-02-28 04:28:06 -05:00
committed by GitHub
parent 0133efe529
commit 91f74471c7
21 changed files with 5563 additions and 34260 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ dev-build/
test-build/
./demo/node_modules
utils/tlsn
.vscode

View File

@@ -16,7 +16,7 @@
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"style-loader": "^4.0.0",
"tlsn-js": "file:../../.."
"tlsn-js": "../../.."
},
"devDependencies": {
"@types/react": "^18.0.26",
@@ -36,4 +36,4 @@
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}
}

View File

@@ -5,6 +5,8 @@ import { Watch } from 'react-loader-spinner';
import { Prover as TProver } from 'tlsn-js';
import { type Method } from 'tlsn-wasm';
import './app.scss';
import { HTTPParser } from 'http-parser-js';
import { Commit, mapStringToRange, subtractRanges } from 'tlsn-js';
const { init, Prover }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
@@ -78,25 +80,42 @@ function App(): ReactElement {
}
try {
const { sent, recv } = transcript;
const {
info: recvInfo,
headers: recvHeaders,
body: recvBody,
} = parseHttpMessage(Buffer.from(recv), 'response');
const body = JSON.parse(recvBody[0].toString());
console.time('reveal');
const reveal = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['connection'],
transcript.ranges.sent.headers!['host'],
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['content-length'],
...transcript.ranges.sent.lineBreaks,
],
const reveal: Commit = {
sent: subtractRanges(
{ start: 0, end: sent.length },
mapStringToRange(
['secret: test_secret'],
Buffer.from(sent).toString('utf-8'),
),
),
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers['server'],
transcript.ranges.recv.headers['date'],
transcript.ranges.recv.headers['content-type'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['eye_color'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
...mapStringToRange(
[
recvInfo,
`${recvHeaders[4]}: ${recvHeaders[5]}\r\n`,
`${recvHeaders[6]}: ${recvHeaders[7]}\r\n`,
`${recvHeaders[8]}: ${recvHeaders[9]}\r\n`,
`${recvHeaders[10]}: ${recvHeaders[11]}\r\n`,
`${recvHeaders[12]}: ${recvHeaders[13]}`,
`${recvHeaders[14]}: ${recvHeaders[15]}`,
`${recvHeaders[16]}: ${recvHeaders[17]}`,
`${recvHeaders[18]}: ${recvHeaders[19]}`,
`"name":"${body.name}"`,
`"gender":"${body.gender}"`,
`"eye_color":"${body.eye_color}"`,
],
Buffer.from(recv).toString('utf-8'),
),
],
};
console.log('Start reveal:', reveal);
@@ -133,10 +152,12 @@ function App(): ReactElement {
<div className="text-center text-gray-700 mb-6">
<p>
Before clicking the <span className="font-semibold">Start</span> button,
make sure the <i>interactive verifier</i> and the{' '}
<i>web socket proxy</i> are running.</p>
<p>Check the{' '}
Before clicking the <span className="font-semibold">Start</span>{' '}
button, make sure the <i>interactive verifier</i> and the{' '}
<i>web socket proxy</i> are running.
</p>
<p>
Check the{' '}
<a href="README.md" className="text-blue-600 hover:underline">
README
</a>{' '}
@@ -208,3 +229,35 @@ function App(): ReactElement {
</div>
);
}
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
const parser = new HTTPParser(
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
);
const body: Buffer[] = [];
let complete = false;
let headers: string[] = [];
parser.onBody = (t) => {
body.push(t);
};
parser.onHeadersComplete = (res) => {
headers = res.headers;
};
parser.onMessageComplete = () => {
complete = true;
};
parser.execute(buffer);
parser.finish();
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
return {
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
headers,
body,
};
}

View File

@@ -14,6 +14,7 @@
"css-loader": "^7.1.2",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"http-parser-js": "^0.5.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",

View File

@@ -8,10 +8,12 @@ import {
Commit,
NotaryServer,
Transcript,
mapStringToRange,
subtractRanges,
} from 'tlsn-js';
import { PresentationJSON } from 'tlsn-js/build/types';
import './app.scss';
import { HTTPParser } from 'http-parser-js';
const { init, Prover, Presentation }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
@@ -22,8 +24,12 @@ const root = createRoot(container!);
root.render(<App />);
const local = true; // Toggle between local and remote notary
const notaryUrl = local ? 'http://localhost:7047' : 'https://notary.pse.dev/v0.1.0-alpha.7';
const websocketProxyUrl = local ? 'ws://localhost:55688' : 'wss://notary.pse.dev/proxy?token=swapi.dev';
const notaryUrl = local
? 'http://localhost:7047'
: 'https://notary.pse.dev/v0.1.0-alpha.7';
const websocketProxyUrl = local
? 'ws://localhost:55688'
: 'wss://notary.pse.dev/proxy?token=swapi.dev';
const loggingLevel = 'Info'; // https://github.com/tlsnotary/tlsn/blob/main/crates/wasm/src/log.rs#L8
const serverUrl = 'https://swapi.dev/api/people/1';
@@ -59,6 +65,7 @@ function App(): ReactElement {
method: 'GET',
headers: {
'Content-Type': 'application/json',
secret: 'test_secret',
},
body: {
hello: 'world',
@@ -71,23 +78,44 @@ function App(): ReactElement {
console.time('transcript');
const transcript = await prover.transcript();
console.log(transcript);
const { sent, recv } = transcript;
console.log(new Transcript({ sent, recv }));
console.timeEnd('transcript');
console.time('commit');
const {
info: recvInfo,
headers: recvHeaders,
body: recvBody,
} = parseHttpMessage(Buffer.from(recv), 'response');
const body = JSON.parse(recvBody[0].toString());
const commit: Commit = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['host'],
...transcript.ranges.sent.lineBreaks,
],
sent: subtractRanges(
{ start: 0, end: sent.length },
mapStringToRange(
['secret: test_secret'],
Buffer.from(sent).toString('utf-8'),
),
),
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers!['server'],
transcript.ranges.recv.headers!['date'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
...mapStringToRange(
[
recvInfo,
`${recvHeaders[4]}: ${recvHeaders[5]}\r\n`,
`${recvHeaders[6]}: ${recvHeaders[7]}\r\n`,
`${recvHeaders[8]}: ${recvHeaders[9]}\r\n`,
`${recvHeaders[10]}: ${recvHeaders[11]}\r\n`,
`${recvHeaders[12]}: ${recvHeaders[13]}`,
`${recvHeaders[14]}: ${recvHeaders[15]}`,
`${recvHeaders[16]}: ${recvHeaders[17]}`,
`${recvHeaders[18]}: ${recvHeaders[19]}`,
`"name":"${body.name}"`,
`"gender":"${body.gender}"`,
],
Buffer.from(recv).toString('utf-8'),
),
],
};
const notarizationOutputs = await prover.notarize(commit);
@@ -164,12 +192,16 @@ function App(): ReactElement {
</h1>
<div className="mb-4 text-base font-light max-w-2xl">
<p>
This demo showcases how to use TLSNotary in a React/TypeScript app with the tlsn-js library.
We will fetch JSON data from the Star Wars API, notarize the TLS request using TLSNotary,
and verify the proof. The demo runs entirely in the browser.
This demo showcases how to use TLSNotary in a React/TypeScript app
with the tlsn-js library. We will fetch JSON data from the Star Wars
API, notarize the TLS request using TLSNotary, and verify the proof.
The demo runs entirely in the browser.
</p>
<p>
<a href="https://docs.tlsnotary.org/quick_start/tlsn-js.html" className="text-blue-500 hover:underline">
<a
href="https://docs.tlsnotary.org/quick_start/tlsn-js.html"
className="text-blue-500 hover:underline"
>
More info
</a>
</p>
@@ -199,7 +231,8 @@ function App(): ReactElement {
<div className="mb-4">
<p className="mb-2 text-base font-light">
There are two versions of the demo: one with a normal config and one with a helper method.
There are two versions of the demo: one with a normal config and one
with a helper method.
</p>
<div className="flex justify-center gap-4">
<button
@@ -273,3 +306,35 @@ function App(): ReactElement {
</div>
);
}
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
const parser = new HTTPParser(
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
);
const body: Buffer[] = [];
let complete = false;
let headers: string[] = [];
parser.onBody = (t) => {
body.push(t);
};
parser.onHeadersComplete = (res) => {
headers = res.headers;
};
parser.onMessageComplete = () => {
complete = true;
};
parser.execute(buffer);
parser.finish();
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
return {
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
headers,
body,
};
}

View File

@@ -20,7 +20,7 @@
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"tailwindcss": "^3.4.14",
"tlsn-js": "0.1.0-alpha.7.1",
"tlsn-js": "../../",
"ws": "^8.18.0"
},
"devDependencies": {

View File

@@ -7,9 +7,12 @@ import {
Verifier as TVerifier,
Commit,
Transcript,
subtractRanges,
mapStringToRange,
} from 'tlsn-js';
import './app.scss';
import WebSocketStream from './stream';
import { HTTPParser } from 'http-parser-js';
const { init, Prover, Verifier }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
@@ -152,26 +155,46 @@ function App(): ReactElement {
addProverLog('Response received');
addProverLog('Transcript sent');
addProverLog(transcript.sent);
addProverLog(Buffer.from(transcript.sent).toString('utf-8'));
addProverLog('Transcript received');
addProverLog(transcript.recv);
addProverLog(Buffer.from(transcript.recv).toString('utf-8'));
addProverLog('Revealing data to verifier');
const { sent, recv } = transcript;
const {
info: recvInfo,
headers: recvHeaders,
body: recvBody,
} = parseHttpMessage(Buffer.from(recv), 'response');
const body = JSON.parse(recvBody[0].toString());
// Prover only reveals parts the transcript to the verifier
const commit: Commit = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['host'],
...transcript.ranges.sent.lineBreaks,
],
sent: subtractRanges(
{ start: 0, end: sent.length },
mapStringToRange(
['secret: test_secret'],
Buffer.from(sent).toString('utf-8'),
),
),
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers!['server'],
transcript.ranges.recv.headers!['date'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
...mapStringToRange(
[
recvInfo,
`${recvHeaders[4]}: ${recvHeaders[5]}\r\n`,
`${recvHeaders[6]}: ${recvHeaders[7]}\r\n`,
`${recvHeaders[8]}: ${recvHeaders[9]}\r\n`,
`${recvHeaders[10]}: ${recvHeaders[11]}\r\n`,
`${recvHeaders[12]}: ${recvHeaders[13]}`,
`${recvHeaders[14]}: ${recvHeaders[15]}`,
`${recvHeaders[16]}: ${recvHeaders[17]}`,
`${recvHeaders[18]}: ${recvHeaders[19]}`,
`"name":"${body.name}"`,
`"gender":"${body.gender}"`,
],
Buffer.from(recv).toString('utf-8'),
),
],
};
await prover.reveal(commit);
@@ -279,3 +302,35 @@ function Button(props: any) {
/>
);
}
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
const parser = new HTTPParser(
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
);
const body: Buffer[] = [];
let complete = false;
let headers: string[] = [];
parser.onBody = (t) => {
body.push(t);
};
parser.onHeadersComplete = (res) => {
headers = res.headers;
};
parser.onMessageComplete = () => {
complete = true;
};
parser.execute(buffer);
parser.finish();
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
return {
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
headers,
body,
};
}

27324
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-js",
"version": "0.1.0-alpha.7.1",
"version": "0.1.0-alpha.7.2",
"description": "",
"repository": "https://github.com/tlsnotary/tlsn-js",
"main": "build/lib.js",
@@ -23,16 +23,17 @@
"lint:eslint": "eslint . --fix",
"lint:tsc": "tsc --noEmit",
"lint": "concurrently npm:lint:tsc npm:lint:eslint",
"run:test": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha -r ts-node/register 'test/testRunner.ts'",
"test": "npm run build:tlsn-binaries && npm run build:test && npm run run:test",
"test:only": "npm run build:test && npm run run:test"
"run:spec": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha -r ts-node/register 'test/specs/*.ts'",
"run:e2e": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha -r ts-node/register 'test/testRunner.ts'",
"test": "npm run build:tlsn-binaries && npm run build:test && npm run run:e2e",
"test:only": "npm run build:test && npm run run:e2e"
},
"devDependencies": {
"@types/mocha": "^10.0.6",
"@types/serve-handler": "^6.1.4",
"browserify": "^17.0.0",
"buffer": "^6.0.3",
"comlink": "^4.4.1",
"comlink": "4.4.1",
"concurrently": "^5.1.0",
"constants-browserify": "^1.0.0",
"copy-webpack-plugin": "^5.0.5",
@@ -43,6 +44,7 @@
"file-loader": "^5.0.2",
"html-webpack-plugin": "~5.3.2",
"https-browserify": "^1.0.0",
"http-parser-js": "^0.5.9",
"image-webpack-loader": "^6.0.0",
"js-yaml": "^4.1.0",
"mocha": "^10.2.0",
@@ -71,4 +73,4 @@
"dependencies": {
"tlsn-wasm": "^0.1.0-alpha.7.2"
}
}
}

11249
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@ Comlink.expose({
```
```ts
// app.ts
import { NotaryServer } from 'tlsn-js';
import { NotaryServer, subtractRanges, mapStringToRange } from 'tlsn-js';
const { init, Prover, NotarizedSession, TlsProof }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
@@ -72,19 +72,15 @@ const transcript = await prover.transcript();
// Select ranges to commit
const commit: Commit = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['host'],
...transcript.ranges.sent.lineBreaks,
],
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
['secret: test_secret'],
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers!['server'],
transcript.ranges.recv.headers!['date'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
{ start: 0, end: transcript.recv.length },
],
};

View File

@@ -19,18 +19,14 @@ import initWasm, {
ConnectionInfo,
PartialTranscript,
} from 'tlsn-wasm';
import {
arrayToHex,
processTranscript,
expect,
headerToMap,
hexToArray,
} from './utils';
import { ParsedTranscriptData, PresentationJSON } from './types';
import { arrayToHex, expect, headerToMap, hexToArray } from './utils';
import { PresentationJSON } from './types';
import { Buffer } from 'buffer';
import { Transcript, subtractRanges, mapStringToRange } from './transcript';
let LOGGING_LEVEL: LoggingLevel = 'Info';
function debug(...args: any[]) {
function debug(...args: unknown[]) {
if (['Debug', 'Trace'].includes(LOGGING_LEVEL)) {
console.log('tlsn-js DEBUG', ...args);
}
@@ -78,7 +74,7 @@ export class Prover {
headers?: {
[name: string]: string;
};
body?: any;
body?: unknown;
maxSentData?: number;
maxRecvData?: number;
maxRecvDataOnline?: number;
@@ -166,27 +162,14 @@ export class Prover {
return this.#prover.setup(verifierUrl);
}
async transcript(): Promise<{
sent: string;
recv: string;
ranges: { recv: ParsedTranscriptData; sent: ParsedTranscriptData };
}> {
async transcript(): Promise<{ sent: number[]; recv: number[] }> {
const transcript = this.#prover.transcript();
const recv = Buffer.from(transcript.recv).toString();
const sent = Buffer.from(transcript.sent).toString();
return {
recv,
sent,
ranges: {
recv: processTranscript(recv),
sent: processTranscript(sent),
},
};
return { sent: transcript.sent, recv: transcript.recv };
}
static getHeaderMap(
url: string,
body?: any,
body?: unknown,
headers?: { [key: string]: string },
) {
const hostname = new URL(url).hostname;
@@ -217,7 +200,7 @@ export class Prover {
url: string;
method?: Method;
headers?: { [key: string]: string };
body?: any;
body?: unknown;
},
): Promise<{
status: number;
@@ -481,45 +464,7 @@ export class NotaryServer {
}
}
export class Transcript {
#sent: number[];
#recv: number[];
constructor(params: { sent: number[]; recv: number[] }) {
this.#recv = params.recv;
this.#sent = params.sent;
}
static processRanges(text: string) {
return processTranscript(text);
}
recv(redactedSymbol = '*') {
return this.#recv.reduce((recv: string, num) => {
recv =
recv + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
return recv;
}, '');
}
sent(redactedSymbol = '*') {
return this.#sent.reduce((sent: string, num) => {
sent =
sent + (num === 0 ? redactedSymbol : Buffer.from([num]).toString());
return sent;
}, '');
}
text = (redactedSymbol = '*') => {
return {
sent: this.sent(redactedSymbol),
recv: this.recv(redactedSymbol),
};
};
}
export {
type ParsedTranscriptData,
type LoggingLevel,
type LoggingConfig,
type Commit,
@@ -530,4 +475,7 @@ export {
type VerifierOutput,
type ConnectionInfo,
type PartialTranscript,
Transcript,
mapStringToRange,
subtractRanges,
};

103
src/transcript.ts Normal file
View File

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

View File

@@ -3,15 +3,6 @@ export type CommitData = {
end: number;
};
export type ParsedTranscriptData = {
all: CommitData;
info: CommitData;
headers: { [key: string]: CommitData };
body?: CommitData;
json?: { [path: string]: CommitData };
lineBreaks: CommitData[];
};
export type PresentationJSON = {
version: '0.1.0-alpha.7';
data: string;

View File

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

View File

@@ -0,0 +1,147 @@
import {
Prover as _Prover,
NotaryServer,
Presentation as _Presentation,
Commit,
mapStringToRange,
subtractRanges,
Transcript,
} from '../../src/lib';
import * as Comlink from 'comlink';
import { assert } from '../utils';
import { HTTPParser } from 'http-parser-js';
const { init, Prover, Presentation }: any = Comlink.wrap(
// @ts-ignore
new Worker(new URL('../worker.ts', import.meta.url)),
);
(async function () {
try {
await init({ loggingLevel: 'Debug' });
// @ts-ignore
console.log('test start');
console.time('prove');
const prover = (await new Prover({
id: 'test',
serverDns: 'swapi.dev',
})) as _Prover;
const notary = NotaryServer.from('http://localhost:7047');
await prover.setup(await notary.sessionUrl());
await prover.sendRequest('wss://notary.pse.dev/proxy?token=swapi.dev', {
url: 'https://swapi.dev/api/people/1',
headers: {
'content-type': 'application/json',
secret: 'test_secret',
},
});
const transcript = await prover.transcript();
const { sent, recv } = transcript;
const {
info: recvInfo,
headers: recvHeaders,
body: recvBody,
} = parseHttpMessage(Buffer.from(recv), 'response');
const body = JSON.parse(recvBody[0].toString());
const commit: Commit = {
sent: subtractRanges(
{ start: 0, end: sent.length },
mapStringToRange(
['secret: test_secret'],
Buffer.from(sent).toString('utf-8'),
),
),
recv: [
...mapStringToRange(
[
recvInfo,
`${recvHeaders[4]}: ${recvHeaders[5]}\r\n`,
`${recvHeaders[6]}: ${recvHeaders[7]}\r\n`,
`${recvHeaders[8]}: ${recvHeaders[9]}\r\n`,
`${recvHeaders[10]}: ${recvHeaders[11]}\r\n`,
`${recvHeaders[12]}: ${recvHeaders[13]}`,
`${recvHeaders[14]}: ${recvHeaders[15]}`,
`${recvHeaders[16]}: ${recvHeaders[17]}`,
`${recvHeaders[18]}: ${recvHeaders[19]}`,
`"name":"${body.name}"`,
`"hair_color":"${body.hair_color}"`,
`"skin_color":"${body.skin_color}"`,
],
Buffer.from(recv).toString('utf-8'),
),
],
};
console.log(commit);
const notarizationOutput = await prover.notarize(commit);
const presentation = (await new Presentation({
attestationHex: notarizationOutput.attestation,
secretsHex: notarizationOutput.secrets,
reveal: commit,
})) as _Presentation;
console.log('presentation:', await presentation.serialize());
console.timeEnd('prove');
console.time('verify');
const { transcript: partialTranscript, server_name } =
await presentation.verify();
const verifyingKey = await presentation.verifyingKey();
console.timeEnd('verify');
console.log('verifyingKey', verifyingKey);
const t = new Transcript({
sent: partialTranscript.sent,
recv: partialTranscript.recv,
});
const sentStr = t.sent();
const recvStr = t.recv();
assert(sentStr.includes('host: swapi.dev'));
assert(!sentStr.includes('secret: test_secret'));
assert(recvStr.includes('"name":"Luke Skywalker"'));
assert(recvStr.includes('"hair_color":"blond"'));
assert(recvStr.includes('"skin_color":"fair"'));
assert(server_name === 'swapi.dev');
// @ts-ignore
document.getElementById('full-integration-swapi').textContent = 'OK';
} catch (err) {
console.log('caught error from wasm');
console.error(err);
// @ts-ignore
document.getElementById('full-integration-swapi').textContent = err.message;
}
})();
function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
const parser = new HTTPParser(
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
);
const body: Buffer[] = [];
let complete = false;
let headers: string[] = [];
parser.onBody = (t) => {
body.push(t);
};
parser.onHeadersComplete = (res) => {
headers = res.headers;
};
parser.onMessageComplete = () => {
complete = true;
};
parser.execute(buffer);
parser.finish();
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
return {
info: buffer.toString('utf-8').split('\r\n')[0] + '\r\n',
headers,
body,
};
}

View File

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

90
test/specs/transcript.ts Normal file
View File

@@ -0,0 +1,90 @@
import { describe, it } from 'mocha';
import * as assert from 'assert';
import { Transcript } from '../../src/transcript';
describe('Transcript parsing', () => {
it('should parse transcript correctly', async () => {
const transcript = new Transcript({ sent: swapiSent, recv: swapiRecv });
assert.strictEqual(
Buffer.from(transcript.raw.sent).toString('utf-8'),
'GET https://swapi.dev/api/people/1 HTTP/1.1\r\nconnection: close\r\ncontent-length: 25\r\ncontent-type: application/json\r\nhost: swapi.dev\r\n\r\n{"hello":"world","one":1}',
);
assert.strictEqual(
Buffer.from(transcript.raw.recv).toString('utf-8'),
'HTTP/1.1 200 OK\r\nServer: nginx/1.16.1\r\nDate: Fri, 07 Feb 2025 07:37:11 GMT\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: close\r\nVary: Accept, Cookie\r\nX-Frame-Options: SAMEORIGIN\r\nETag: \"ee398610435c328f4d0a4e1b0d2f7bbc\"\r\nAllow: GET, HEAD, OPTIONS\r\nStrict-Transport-Security: max-age=15768000\r\n\r\n287\r\n{\"name\":\"Luke Skywalker\",\"height\":\"172\",\"mass\":\"77\",\"hair_color\":\"blond\",\"skin_color\":\"fair\",\"eye_color\":\"blue\",\"birth_year\":\"19BBY\",\"gender\":\"male\",\"homeworld\":\"https://swapi.dev/api/planets/1/\",\"films\":[\"https://swapi.dev/api/films/1/\",\"https://swapi.dev/api/films/2/\",\"https://swapi.dev/api/films/3/\",\"https://swapi.dev/api/films/6/\"],\"species\":[],\"vehicles\":[\"https://swapi.dev/api/vehicles/14/\",\"https://swapi.dev/api/vehicles/30/\"],\"starships\":[\"https://swapi.dev/api/starships/12/\",\"https://swapi.dev/api/starships/22/\"],\"created\":\"2014-12-09T13:50:51.644000Z\",\"edited\":\"2014-12-20T21:17:56.891000Z\",\"url\":\"https://swapi.dev/api/people/1/\"}\r\n0\r\n\r\n',
);
});
});
const swapiRecv = [
72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75, 13, 10, 83, 101,
114, 118, 101, 114, 58, 32, 110, 103, 105, 110, 120, 47, 49, 46, 49, 54, 46,
49, 13, 10, 68, 97, 116, 101, 58, 32, 70, 114, 105, 44, 32, 48, 55, 32, 70,
101, 98, 32, 50, 48, 50, 53, 32, 48, 55, 58, 51, 55, 58, 49, 49, 32, 71, 77,
84, 13, 10, 67, 111, 110, 116, 101, 110, 116, 45, 84, 121, 112, 101, 58, 32,
97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 106, 115, 111, 110,
13, 10, 84, 114, 97, 110, 115, 102, 101, 114, 45, 69, 110, 99, 111, 100, 105,
110, 103, 58, 32, 99, 104, 117, 110, 107, 101, 100, 13, 10, 67, 111, 110, 110,
101, 99, 116, 105, 111, 110, 58, 32, 99, 108, 111, 115, 101, 13, 10, 86, 97,
114, 121, 58, 32, 65, 99, 99, 101, 112, 116, 44, 32, 67, 111, 111, 107, 105,
101, 13, 10, 88, 45, 70, 114, 97, 109, 101, 45, 79, 112, 116, 105, 111, 110,
115, 58, 32, 83, 65, 77, 69, 79, 82, 73, 71, 73, 78, 13, 10, 69, 84, 97, 103,
58, 32, 34, 101, 101, 51, 57, 56, 54, 49, 48, 52, 51, 53, 99, 51, 50, 56, 102,
52, 100, 48, 97, 52, 101, 49, 98, 48, 100, 50, 102, 55, 98, 98, 99, 34, 13,
10, 65, 108, 108, 111, 119, 58, 32, 71, 69, 84, 44, 32, 72, 69, 65, 68, 44,
32, 79, 80, 84, 73, 79, 78, 83, 13, 10, 83, 116, 114, 105, 99, 116, 45, 84,
114, 97, 110, 115, 112, 111, 114, 116, 45, 83, 101, 99, 117, 114, 105, 116,
121, 58, 32, 109, 97, 120, 45, 97, 103, 101, 61, 49, 53, 55, 54, 56, 48, 48,
48, 13, 10, 13, 10, 50, 56, 55, 13, 10, 123, 34, 110, 97, 109, 101, 34, 58,
34, 76, 117, 107, 101, 32, 83, 107, 121, 119, 97, 108, 107, 101, 114, 34, 44,
34, 104, 101, 105, 103, 104, 116, 34, 58, 34, 49, 55, 50, 34, 44, 34, 109, 97,
115, 115, 34, 58, 34, 55, 55, 34, 44, 34, 104, 97, 105, 114, 95, 99, 111, 108,
111, 114, 34, 58, 34, 98, 108, 111, 110, 100, 34, 44, 34, 115, 107, 105, 110,
95, 99, 111, 108, 111, 114, 34, 58, 34, 102, 97, 105, 114, 34, 44, 34, 101,
121, 101, 95, 99, 111, 108, 111, 114, 34, 58, 34, 98, 108, 117, 101, 34, 44,
34, 98, 105, 114, 116, 104, 95, 121, 101, 97, 114, 34, 58, 34, 49, 57, 66, 66,
89, 34, 44, 34, 103, 101, 110, 100, 101, 114, 34, 58, 34, 109, 97, 108, 101,
34, 44, 34, 104, 111, 109, 101, 119, 111, 114, 108, 100, 34, 58, 34, 104, 116,
116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105, 46, 100, 101, 118, 47, 97,
112, 105, 47, 112, 108, 97, 110, 101, 116, 115, 47, 49, 47, 34, 44, 34, 102,
105, 108, 109, 115, 34, 58, 91, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
109, 115, 47, 49, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
109, 115, 47, 50, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
109, 115, 47, 51, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115,
119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 102, 105, 108,
109, 115, 47, 54, 47, 34, 93, 44, 34, 115, 112, 101, 99, 105, 101, 115, 34,
58, 91, 93, 44, 34, 118, 101, 104, 105, 99, 108, 101, 115, 34, 58, 91, 34,
104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105, 46, 100, 101,
118, 47, 97, 112, 105, 47, 118, 101, 104, 105, 99, 108, 101, 115, 47, 49, 52,
47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105,
46, 100, 101, 118, 47, 97, 112, 105, 47, 118, 101, 104, 105, 99, 108, 101,
115, 47, 51, 48, 47, 34, 93, 44, 34, 115, 116, 97, 114, 115, 104, 105, 112,
115, 34, 58, 91, 34, 104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112,
105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 115, 116, 97, 114, 115, 104,
105, 112, 115, 47, 49, 50, 47, 34, 44, 34, 104, 116, 116, 112, 115, 58, 47,
47, 115, 119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 115, 116,
97, 114, 115, 104, 105, 112, 115, 47, 50, 50, 47, 34, 93, 44, 34, 99, 114,
101, 97, 116, 101, 100, 34, 58, 34, 50, 48, 49, 52, 45, 49, 50, 45, 48, 57,
84, 49, 51, 58, 53, 48, 58, 53, 49, 46, 54, 52, 52, 48, 48, 48, 90, 34, 44,
34, 101, 100, 105, 116, 101, 100, 34, 58, 34, 50, 48, 49, 52, 45, 49, 50, 45,
50, 48, 84, 50, 49, 58, 49, 55, 58, 53, 54, 46, 56, 57, 49, 48, 48, 48, 90,
34, 44, 34, 117, 114, 108, 34, 58, 34, 104, 116, 116, 112, 115, 58, 47, 47,
115, 119, 97, 112, 105, 46, 100, 101, 118, 47, 97, 112, 105, 47, 112, 101,
111, 112, 108, 101, 47, 49, 47, 34, 125, 13, 10, 48, 13, 10, 13, 10,
];
const swapiSent = [
71, 69, 84, 32, 104, 116, 116, 112, 115, 58, 47, 47, 115, 119, 97, 112, 105,
46, 100, 101, 118, 47, 97, 112, 105, 47, 112, 101, 111, 112, 108, 101, 47, 49,
32, 72, 84, 84, 80, 47, 49, 46, 49, 13, 10, 99, 111, 110, 110, 101, 99, 116,
105, 111, 110, 58, 32, 99, 108, 111, 115, 101, 13, 10, 99, 111, 110, 116, 101,
110, 116, 45, 108, 101, 110, 103, 116, 104, 58, 32, 50, 53, 13, 10, 99, 111,
110, 116, 101, 110, 116, 45, 116, 121, 112, 101, 58, 32, 97, 112, 112, 108,
105, 99, 97, 116, 105, 111, 110, 47, 106, 115, 111, 110, 13, 10, 104, 111,
115, 116, 58, 32, 115, 119, 97, 112, 105, 46, 100, 101, 118, 13, 10, 13, 10,
123, 34, 104, 101, 108, 108, 111, 34, 58, 34, 119, 111, 114, 108, 100, 34, 44,
34, 111, 110, 101, 34, 58, 49, 125,
];

View File

@@ -145,7 +145,7 @@ after(async function () {
});
describe('tlsn-js test suite', function () {
fs.readdirSync(path.join(__dirname, 'specs')).forEach((file) => {
fs.readdirSync(path.join(__dirname, 'e2e')).forEach((file) => {
const [id] = file.split('.');
it(`Test ID: ${id}`, async function () {
const content = await check(id);

View File

@@ -37,8 +37,8 @@ module.exports = [
target: 'web',
mode: isProd ? 'production' : 'development',
entry: {
'full-integration-swapi.spec': path.join(__dirname, 'test', 'specs', 'full-integration-swapi.spec.ts'),
'simple-verify': path.join(__dirname, 'test', 'specs', 'simple-verify.spec.ts'),
'full-integration-swapi.spec': path.join(__dirname, 'test', 'e2e', 'full-integration-swapi.spec.ts'),
'simple-verify': path.join(__dirname, 'test', 'e2e', 'simple-verify.spec.ts'),
},
output: {
path: __dirname + '/test-build',