initial draft for tlsn-plugin boilerplate template

This commit is contained in:
Hendrik Eeckhaut
2024-06-25 15:05:48 +02:00
commit 51d19ffeb4
14 changed files with 668 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
/target
**/*.rs.bk
Cargo.lock
bin/
**/node_modules
**/.DS_Store
.idea
build
tlsn/
zip
dist/

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# Plugin Development for the TLSNotary Browser Extension
This repository contains the boilerplate code for developing a plugin for the TLSNotary browser extension. Currently, the template includes a TypeScript-based plugin example that proves ownership of a Twitter profile.
TLSNotary's plugin system uses [Extism](https://github.com/extism), which enables plugins in different programming languages. For more documentation, check [Extism's documentation](https://github.com/extism/js-pdk).
## Installation of Extism-js
1. **Download and Install Extism-js**: Begin by setting up `extism-js`, which enables you to compile and manage your plugins. Run these commands to download and install it:
```sh
curl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh
sh install.sh
```
This script installs the Extism JavaScript Plugin Development Kit from its GitHub repository, preparing your environment for plugin compilation.
## Building the Twitter Profile Plugin
To build the plugin, run:
```sh
npm i
npm run build
```
This will output the wasm binary in `dist/index.wasm`.
### Running the Twitter Plugin Example
1. Build the `twitter_profile` plugin as explained above.
2. Build and install the `tlsn-extension` as documented in the [main README.md](../README.md).
3. [Run a local notary server](https://github.com/tlsnotary/tlsn/blob/main/notary-server/README.md), ensuring `TLS` is disabled in the [config file](https://github.com/tlsnotary/tlsn/blob/main/notary-server/config/config.yaml#L18).
4. Install the plugin: Click the **Add a Plugin (+)** button and select the `index.wasm` file you built in step 1. A **Twitter Profile** button should then appear below the default buttons.
5. Click the **Twitter Profile** button. This action opens the Twitter webpage along with a TLSNotary sidebar.
6. Follow the steps in the TLSNotary sidebar.
7. Access the TLSNotary results by clicking the **History** button in the TLSNotary extension.
## Customize the Template
You are now ready to develop your own plugins.
Notarization requests take more time than regular requests, so we recommend testing the HTTPS request in Postman or Curl first. This allows you to figure out faster which cookies and headers are required.
When you add or rename steps, make sure to update the `index.d.ts` file.
### Custom Icon
To use a custom icon, replace `icon.png` in the `./assets` folder. Make sure it is 320x320 pixels. You can use the following command:
```sh
convert icon_source.png -resize 320x320! assets/icon.png
```
## More Examples
Check the `examples` folder for more examples.

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

39
config.json Normal file
View File

@@ -0,0 +1,39 @@
{
"title": "Twitter Profile",
"description": "Notarize ownership of a twitter profile",
"steps": [
{
"title": "Visit Twitter website",
"cta": "Go to x.com",
"action": "start"
},
{
"title": "Collect credentials",
"description": "Login to your account if you haven't already",
"cta": "Check cookies",
"action": "two"
},
{
"title": "Notarize twitter profile",
"cta": "Notarize",
"action": "three",
"prover": true
}
],
"hostFunctions": [
"redirect",
"notarize"
],
"cookies": [
"api.x.com"
],
"headers": [
"api.x.com"
],
"requests": [
{
"url": "https: //api.x.com/1.1/account/settings.json",
"method": "GET"
}
]
}

85
esbuild.js Normal file
View File

@@ -0,0 +1,85 @@
const esbuild = require('esbuild');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const { name } = require('./package.json');
const { execSync } = require('child_process');
// Promisify fs.readFile and fs.stat for convenience
const readFileAsync = promisify(fs.readFile);
const statAsync = promisify(fs.stat);
const mkdirAsync = promisify(fs.mkdir);
/**
* Generates a Base64 encoded icon file.
* It checks if the output file already exists and is up-to-date before generating a new one.
*/
async function generateBase64Icon() {
const iconPath = path.join(__dirname, 'assets', "icon.png");
const outputDir = path.join(__dirname, 'dist', 'assets');
const outputPath = path.join(outputDir, 'icon.ts');
try {
// Ensure the output directory exists
await mkdirAsync(outputDir, { recursive: true });
const [iconStat, outputStat] = await Promise.all([
statAsync(iconPath).catch(() => null),
statAsync(outputPath).catch(() => null)
]);
// Check if output file exists and is newer than the icon file
if (outputStat && iconStat && outputStat.mtime > iconStat.mtime) {
console.log('Base64 icon file is up-to-date.');
return;
}
const fileBuffer = await readFileAsync(iconPath);
const base64Icon = `data:image/png;base64,${fileBuffer.toString('base64')}`;
const outputContent = `// This is a generated file. Do not edit directly.
// This is a Base64 encoded version of the plugin's icon ('icon.png') used in the plugin's config.
// This file is automatically generated by esBuild.js whenever the icon is changed.
// There is no need to add it to version control.
export const icon = "${base64Icon}";\n`;
fs.writeFileSync(outputPath, outputContent);
console.log('Base64 icon file generated successfully.');
} catch (error) {
console.error(`Failed to generate base64 icon: ${error.message}`);
process.exit(1);
}
}
const outputDir = 'dist';
const entryFile = 'src/index.ts';
const outputFile = path.join(outputDir, 'index.js');
const outputWasm = path.join(outputDir, `${name}.tlsn.wasm`);
async function build() {
await generateBase64Icon();
try {
await esbuild.build({
entryPoints: [entryFile],
bundle: true,
outdir: outputDir, // Use outdir for directory output
sourcemap: true,
minify: false, // might want to use true for production build
format: 'cjs', // needs to be CJS for now
target: ['es2020'], // don't go over es2020 because quickjs doesn't support it
});
console.log('esbuild completed successfully.');
// Run extism-js to generate the wasm file
const extismCommand = `extism-js ${outputFile} -i src/index.d.ts -o ${outputWasm}`;
execSync(extismCommand, { stdio: 'inherit' });
console.log('extism-js completed successfully.');
} catch (error) {
console.error('Build process failed:', error);
process.exit(1);
}
}
build();

View File

@@ -0,0 +1,11 @@
# TLSNotary TypeScript plugin demo: Prove Twitter DM
This is a demo demo plugin for the TLSNotary browser extension in plain Javascript.
## Building
Build the plugin:
```
extism-js index.js -i index.d.ts -o index.wasm
```
This command compiles the JavaScript code in index.js into a WebAssembly module, ready for integration with the TLSNotary extension.

14
examples/twitter_dm_js/index.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
declare module 'main' {
// Extism exports take no params and return an I32
export function start(): I32;
export function two(): I32;
export function three(): I32;
export function config(): I32;
}
declare module 'extism:host' {
interface user {
redirect(ptr: I64): void;
notarize(ptr: I64): I64;
}
}

File diff suppressed because one or more lines are too long

15
examples/twitter_profile_js/index.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
declare module 'main' {
// Extism exports take no params and return an I32
export function start(): I32;
export function two(): I32;
export function parseTwitterResp(): I32;
export function three(): I32;
export function config(): I32;
}
declare module 'extism:host' {
interface user {
redirect(ptr: I64): void;
notarize(ptr: I64): I64;
}
}

File diff suppressed because one or more lines are too long

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "twitter_profile",
"version": "1.0.0",
"description": "Demo TLSNotary plugin to notarize the ownership of a twitter profile",
"main": "src/index.ts",
"scripts": {
"build": "node esbuild.js"
},
"keywords": [],
"author": "TLSNotary",
"license": "MIT",
"devDependencies": {
"@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6",
"typescript": "^5.3.2"
}
}

15
src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
declare module 'main' {
// Extism exports take no params and return an I32
export function start(): I32;
export function two(): I32;
export function parseTwitterResp(): I32;
export function three(): I32;
export function config(): I32;
}
declare module 'extism:host' {
interface user {
redirect(ptr: I64): void;
notarize(ptr: I64): I64;
}
}

133
src/index.ts Normal file
View File

@@ -0,0 +1,133 @@
import { icon } from '../dist/assets/icon';
import config_json from '../config.json';
/**
* Plugin configuration
* This configurations defines the plugin, most importantly:
* * the different steps
* * the user data (headers, cookies) it will access
* * the web requests it will query (or notarize)
*/
export function config() {
Host.outputString(
JSON.stringify({
...config_json,
icon: icon
}),
);
}
function isValidHost(urlString: string) {
const url = new URL(urlString);
return url.hostname === 'twitter.com' || url.hostname === 'x.com';
}
/**
* Redirect the browser window to x.com
* This uses the `redirect` host function (see index.d.ts)
*/
function gotoTwitter() {
const { redirect } = Host.getFunctions() as any;
const mem = Memory.fromString('https://x.com');
redirect(mem.offset);
}
/**
* Implementation of the first (start) plugin step
*/
export function start() {
if (!isValidHost(Config.get('tabUrl'))) {
gotoTwitter();
Host.outputString(JSON.stringify(false));
return;
}
Host.outputString(JSON.stringify(true));
}
/**
* Implementation of step "two".
* This step collects and validates authentication cookies and headers for 'api.x.com'.
* If all required information, it creates the request object.
* Note that the url needs to be specified in the `config` too, otherwise the request will be refused.
*/
export function two() {
const cookies = JSON.parse(Config.get('cookies'))['api.x.com'];
const headers = JSON.parse(Config.get('headers'))['api.x.com'];
if (
!cookies.auth_token ||
!cookies.ct0 ||
!headers['x-csrf-token'] ||
!headers['authorization']
) {
Host.outputString(JSON.stringify(false));
return;
}
Host.outputString(
JSON.stringify({
url: 'https://api.x.com/1.1/account/settings.json',
method: 'GET',
headers: {
'x-twitter-client-language': 'en',
'x-csrf-token': headers['x-csrf-token'],
Host: 'api.x.com',
authorization: headers.authorization,
Cookie: `lang=en; auth_token=${cookies.auth_token}; ct0=${cookies.ct0}`,
'Accept-Encoding': 'identity',
Connection: 'close',
},
secretHeaders: [
`x-csrf-token: ${headers['x-csrf-token']}`,
`cookie: lang=en; auth_token=${cookies.auth_token}; ct0=${cookies.ct0}`,
`authorization: ${headers.authorization}`,
],
}),
);
}
/**
* This method is used to parse the Twitter response and specify what information is revealed (i.e. **not** redacted)
* This method is optional in the notarization request. When it is not specified nothing is redacted.
*
* In this example it locates the `screen_name` and excludes that range from the revealed response.
*/
export function parseTwitterResp() {
const bodyString = Host.inputString();
const params = JSON.parse(bodyString);
// console.log("params");
// console.log(JSON.stringify(params));
if (params.screen_name) {
const revealed = `"screen_name":"${params.screen_name}"`;
const selectionStart = bodyString.indexOf(revealed);
const selectionEnd =
selectionStart + revealed.length;
const secretResps = [
bodyString.substring(0, selectionStart),
bodyString.substring(selectionEnd, bodyString.length),
];
Host.outputString(JSON.stringify(secretResps));
} else {
Host.outputString(JSON.stringify(false));
}
}
/**
* Step 3: calls the `notarize` host function
*/
export function three() {
const params = JSON.parse(Host.inputString());
const { notarize } = Host.getFunctions() as any;
if (!params) {
Host.outputString(JSON.stringify(false));
} else {
const mem = Memory.fromString(JSON.stringify({
...params,
getSecretResponse: 'parseTwitterResp',
}));
const idOffset = notarize(mem.offset);
const id = Memory.find(idOffset).readString();
Host.outputString(JSON.stringify(id));
}
}

14
tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"lib": [],
"types": [
"@extism/js-pdk"
],
"noEmit": true,
"resolveJsonModule": true,
"esModuleInterop": true,
},
"include": [
"src/**/*"
]
}