Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,947 changes: 150 additions & 3,797 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@
"dependencies": {
"buffer": "^6.0.3",
"comlink": "^4.4.2",
"tlsn-js": "0.1.0-alpha.12.0"
"@csfloat/tlsn-wasm": "0.1.0-alpha.14"
}
}
4 changes: 2 additions & 2 deletions src/environment.dev.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export const environment = {
csfloat_base_api_url: 'http://localhost:8080/api',
notary: {
tlsn: 'https://notary.csfloat.com/tlsn/',
ws: 'wss://notary.csfloat.com/ws/',
tlsn: 'https://notary.csfloat.com',
ws: 'wss://notary.csfloat.com/proxy',
loggingLevel: 'Error',
},
};
4 changes: 2 additions & 2 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export const environment = {
csfloat_base_api_url: 'https://csfloat.com/api',
notary: {
tlsn: 'https://notary.csfloat.com/tlsn/',
ws: 'wss://notary.csfloat.com/ws/',
tlsn: 'https://notary.csfloat.com',
ws: 'wss://notary.csfloat.com/proxy',
loggingLevel: 'Warn',
},
};
10 changes: 4 additions & 6 deletions src/lib/bridge/handlers/notary_prove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ import {SendToOffscreen} from '../../../offscreen/client';
import {HasPermissions} from './has_permissions';
import {NotaryProveRequest} from '../../notary/types';
import {getAccessToken} from '../../alarms/access_token';
import {PresentationJSON} from 'tlsn-js/build/types';
import {
OffscreenRequestType,
TLSNProveOffscreenRequest,
TLSNProveOffscreenResponse,
VerificationResults,
} from '../../../offscreen/handlers/types';
import {MaxConcurrency} from '../wrappers/cached';

export interface NotaryProveResponse {
presentation: PresentationJSON;
}
export interface NotaryProveResponse extends VerificationResults {}

export const NotaryProve = MaxConcurrency(
new SimpleHandler<NotaryProveRequest, NotaryProveResponse>(
Expand All @@ -31,7 +29,7 @@ export const NotaryProve = MaxConcurrency(
throw new Error('must have api.steampowered.com permissions in order to prove API requests');
}

const access_token = await getAccessToken(request.expected_steam_id);
const access_token = await getAccessToken(request.meta?.expected_steam_id);

const response = await SendToOffscreen<TLSNProveOffscreenRequest, TLSNProveOffscreenResponse>(
OffscreenRequestType.TLSN_PROVE,
Expand All @@ -42,7 +40,7 @@ export const NotaryProve = MaxConcurrency(
);

return {
presentation: response.presentation,
payload: response.payload,
};
}
),
Expand Down
5 changes: 4 additions & 1 deletion src/lib/notary/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ interface ProveRequestPayloads {
export type NotaryProveRequest = {
[T in ProofType]: {
type: T;
expected_steam_id?: string;
meta?: {
expected_steam_id?: string;
notary_token?: string;
};
} & ProveRequestPayloads[T];
}[ProofType];
4 changes: 4 additions & 0 deletions src/lib/notary/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export function getSteamRequestURL(request: NotaryProveRequest, access_token: Ac
// Separate the 'type' property from the actual URL parameters
const {type, ...params} = request;
const baseUrl = PROOF_BASE_URLS[type];

// internal field
delete params.meta;

const queryString = buildQueryString(Object.assign(params, {access_token: access_token.token}));
return `${baseUrl}${queryString}`;
}
169 changes: 113 additions & 56 deletions src/offscreen/handlers/notary_prove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import {
import {getSteamRequestURL} from '../../lib/notary/utils';
import * as Comlink from 'comlink';
import {environment} from '../../environment';
import {
Prover as TProver,
Presentation as TPresentation,
Commit,
NotaryServer,
mapStringToRange,
subtractRanges,
} from 'tlsn-js';
const {init, Prover, Presentation}: any = Comlink.wrap(new Worker(new URL('../worker.ts', import.meta.url)));
import type {LoggingLevel, Method, Prover as TProver, Reveal} from '@csfloat/tlsn-wasm';
import {NotarySessionClient} from './notary_session_client';
import {Remote} from 'comlink';

const {init, Prover}: any = Comlink.wrap(new Worker(new URL('../worker.ts', import.meta.url)));

export async function initThreads() {
await init({
loggingLevel: environment.notary.loggingLevel,
loggingLevel: environment.notary.loggingLevel as LoggingLevel,
hardwareConcurrency: navigator.hardwareConcurrency,
crateFilters: [
{name: 'yamux', level: 'Info'},
{name: 'uid_mux', level: 'Info'},
],
});
}

Expand Down Expand Up @@ -49,52 +49,65 @@ export const TLSNProveOffscreenHandler = new ClosableOffscreenHandler<

const maxRecvData = await calculateResponseSize(serverURL, 'GET', headers);

const notary = NotaryServer.from(environment.notary.tlsn);

const prover = (await new Prover({
serverDns: 'api.steampowered.com',
maxRecvData,
maxSentData,
})) as TProver;

await prover.setup(await notary.sessionUrl());

await prover.sendRequest(environment.notary.ws, {
url: serverURL,
method: 'GET',
headers: {
'Accept-Encoding': 'gzip',
},
});

const transcript = await prover.transcript();
const {sent, recv} = transcript;

const commit: Commit = {
sent: subtractRanges(
{start: 0, end: sent.length},
mapStringToRange([request.access_token.token], Buffer.from(sent).toString('utf-8'))
),
recv: [
// No secrets in response body
{start: 0, end: recv.length},
],
};
const notarizationOutputs = await prover.notarize(commit);

const presentation = (await new Presentation({
attestationHex: notarizationOutputs.attestation,
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: {...commit, server_identity: false},
})) as TPresentation;

const presentationJSON = await presentation.json();

return {
presentation: presentationJSON,
};
const maybeNotaryToken = request.notary_request.meta?.notary_token;

const session = await NotarySessionClient.create(maxRecvData, maxSentData, maybeNotaryToken);

try {
// Create and setup prover
const prover = (await new Prover({
server_name: 'api.steampowered.com',
max_recv_data: maxRecvData,
max_sent_data: maxSentData,
network: 'Latency',
defer_decryption_from_start: true,
})) as Remote<TProver>;

let verifierUrl = `${environment.notary.tlsn}/verifier?sessionId=${session.getID()}`;
if (maybeNotaryToken) {
verifierUrl += `&token=${maybeNotaryToken}`;
}

await prover.setup(verifierUrl);

// Convert headers to Map<string, number[]> for WASM
const headerMap = new Map<string, number[]>();
for (const [key, value] of Object.entries(headers)) {
headerMap.set(key, Buffer.from(value).toJSON().data);
}

let wsUrl = `${environment.notary.ws}`;
if (maybeNotaryToken) {
wsUrl += `?token=${maybeNotaryToken}`;
}

// Send HTTP request via proxy
await prover.send_request(wsUrl, {
uri: serverURL,
method: 'GET' as Method,
headers: headerMap,
body: undefined,
});

const {sent, recv} = await prover.transcript();
const sentStr = Buffer.from(sent).toString('utf-8');

// Compute reveal ranges (hide the access token)
const secretRanges = mapStringToRange([request.access_token.token], sentStr);
const sentRanges = subtractRanges({start: 0, end: sent.length}, secretRanges);
const recvRanges = [{start: 0, end: recv.length}];

// Set up listener before calling reveal
const completedPromise = session.finalizeResults();

// Reveal to verifier
const reveal: Reveal = {sent: sentRanges, recv: recvRanges, server_identity: true};
await prover.reveal(reveal);

return await completedPromise;
} finally {
session.close();
}
},
() => {
// Require the offscreen to be re-initialized after every 5 prove requests
Expand Down Expand Up @@ -179,3 +192,47 @@ async function calculateResponseSize(

return headersSize + bodySize;
}

/**
* Computes ranges that hide specified strings from the transcript
*/
function subtractRanges(
fullRange: {start: number; end: number},
secretRanges: {start: number; end: number}[]
): {start: number; end: number}[] {
const sorted = [...secretRanges].sort((a, b) => a.start - b.start);
const result: {start: number; end: number}[] = [];
let currentPos = fullRange.start;

for (const secret of sorted) {
if (secret.start > currentPos) {
result.push({start: currentPos, end: secret.start});
}
currentPos = Math.max(currentPos, secret.end);
}

if (currentPos < fullRange.end) {
result.push({start: currentPos, end: fullRange.end});
}

return result;
}

/**
* Maps strings to their byte ranges in the transcript
*/
function mapStringToRange(secrets: string[], transcript: string): {start: number; end: number}[] {
const ranges: {start: number; end: number}[] = [];
for (const secret of secrets) {
if (!secret) {
continue;
}

let pos = 0;
while ((pos = transcript.indexOf(secret, pos)) !== -1) {
ranges.push({start: pos, end: pos + secret.length});
pos += secret.length;
}
}
return ranges;
}
Loading