feat: integrate with v0.1.0-alpha.6 (#27)

This commit is contained in:
tsukino
2024-08-16 07:37:42 -04:00
committed by GitHub
parent ec163171cb
commit 259f8136eb
25 changed files with 12738 additions and 192 deletions

2
.gitignore vendored
View File

@@ -4,4 +4,4 @@
.idea
build
.env
rs/verifier/target
rs/**/target

29
package-lock.json generated
View File

@@ -36,7 +36,8 @@
"redux-thunk": "^2.4.2",
"stream": "^0.0.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "^0.1.0-alpha.5.2"
"tlsn-js": "0.1.0-alpha.6.2",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
},
"devDependencies": {
"@babel/core": "^7.20.12",
@@ -21931,9 +21932,18 @@
}
},
"node_modules/tlsn-js": {
"version": "0.1.0-alpha.5.2",
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.2.tgz",
"integrity": "sha512-5QEAzjOPLrw7TN5nRYphUbVBihZ2ufIQ+BcKnTw+0MejqZHo9pUcjHGkrkt9EfhdkC2zsWWKbDZXlAPxwaPtOQ==",
"version": "0.1.0-alpha.6.2",
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.6.2.tgz",
"integrity": "sha512-6PANWQEFw48VFNfDgA017zcNRae2OutzNmE5Xcc/h6lwkZZ8MBZIFAFfz/WHZJ3fcFJumaKrG80gpomP6blZqw==",
"engines": {
"node": ">= 16.20.2"
}
},
"node_modules/tlsn-js-v5": {
"name": "tlsn-js",
"version": "0.1.0-alpha.5.4",
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.4.tgz",
"integrity": "sha512-qbqaDjApXarohn/XMJrxMsNHYTCzy+pw0Fc8gtPPN17PLE+1DwwLTXAAhnxYqYQyo3+Hmy+ODd4C02+KCwRWmg==",
"dependencies": {
"comlink": "^4.4.1"
},
@@ -39401,9 +39411,14 @@
"optional": true
},
"tlsn-js": {
"version": "0.1.0-alpha.5.2",
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.2.tgz",
"integrity": "sha512-5QEAzjOPLrw7TN5nRYphUbVBihZ2ufIQ+BcKnTw+0MejqZHo9pUcjHGkrkt9EfhdkC2zsWWKbDZXlAPxwaPtOQ==",
"version": "0.1.0-alpha.6.2",
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.6.2.tgz",
"integrity": "sha512-6PANWQEFw48VFNfDgA017zcNRae2OutzNmE5Xcc/h6lwkZZ8MBZIFAFfz/WHZJ3fcFJumaKrG80gpomP6blZqw=="
},
"tlsn-js-v5": {
"version": "npm:tlsn-js@0.1.0-alpha.5.4",
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.4.tgz",
"integrity": "sha512-qbqaDjApXarohn/XMJrxMsNHYTCzy+pw0Fc8gtPPN17PLE+1DwwLTXAAhnxYqYQyo3+Hmy+ODd4C02+KCwRWmg==",
"requires": {
"comlink": "^4.4.1"
}

View File

@@ -54,7 +54,8 @@
"redux-thunk": "^2.4.2",
"stream": "^0.0.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "^0.1.0-alpha.5.2"
"tlsn-js": "0.1.0-alpha.6.2",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
},
"devDependencies": {
"@babel/core": "^7.20.12",

10679
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1526
rs/0.1.0-alpha.6/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
[package]
name = "verifier"
version = "0.1.0"
license = "ISC"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "0.4"
neon = "1"
elliptic-curve = { version = "0.13.5", features = ["pkcs8"] }
p256 = { version = "0.13", features = ["pem", "ecdsa"] }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0"
hex = "0.4"
bincode = { version = "1.3" }
tlsn-core = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.6", package = "tlsn-core" }

119
rs/0.1.0-alpha.6/README.md Normal file
View File

@@ -0,0 +1,119 @@
# verifier
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
## Installing verifier
Installing verifier requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
You can install the project with npm. In the project directory, run:
```sh
$ npm install
```
This fully installs the project, including installing any dependencies and running the build.
## Building verifier
If you have already installed the project and only want to run the build, run:
```sh
$ npm run build
```
This command uses the [cargo-cp-artifact](https://github.com/neon-bindings/cargo-cp-artifact) utility to run the Rust build and copy the built library into `./index.node`.
## Exploring verifier
After building verifier, you can explore its exports at the Node REPL:
```sh
$ npm install
$ node
> require('.').hello()
"hello node"
```
## Available Scripts
In the project directory, you can run:
### `npm install`
Installs the project, including running `npm run build`.
### `npm build`
Builds the Node addon (`index.node`) from source.
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm build` and `npm build-*` commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
```
npm run build -- --feature=beetle
```
#### `npm build-debug`
Alias for `npm build`.
#### `npm build-release`
Same as [`npm build`](#npm-build) but, builds the module with the [`release`](https://doc.rust-lang.org/cargo/reference/profiles.html#release) profile. Release builds will compile slower, but run faster.
### `npm test`
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
## Project Layout
The directory structure of this project is:
```
verifier/
├── Cargo.toml
├── README.md
├── index.node
├── package.json
├── src/
| └── lib.rs
└── target/
```
### Cargo.toml
The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command.
### README.md
This file.
### index.node
The Node addon—i.e., a binary Node module—generated by building the project. This is the main module for this package, as dictated by the `"main"` key in `package.json`.
Under the hood, a [Node addon](https://nodejs.org/api/addons.html) is a [dynamically-linked shared object](https://en.wikipedia.org/wiki/Library_(computing)#Shared_libraries). The `"build"` script produces this file by copying it from within the `target/` directory, which is where the Rust build produces the shared object.
### package.json
The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command.
### src/
The directory tree containing the Rust source code for the project.
### src/lib.rs
The Rust library's main module.
### target/
Binary artifacts generated by the Rust build.
## Learn More
To learn more about Neon, see the [Neon documentation](https://neon-bindings.com).
To learn more about Rust, see the [Rust documentation](https://www.rust-lang.org).
To learn more about Node, see the [Node documentation](https://nodejs.org).

BIN
rs/0.1.0-alpha.6/index.node Executable file

Binary file not shown.

34
rs/0.1.0-alpha.6/package-lock.json generated Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "verifier",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "verifier",
"version": "0.1.0",
"hasInstallScript": true,
"license": "ISC",
"devDependencies": {
"cargo-cp-artifact": "^0.1"
}
},
"node_modules/cargo-cp-artifact": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.9.tgz",
"integrity": "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA==",
"dev": true,
"bin": {
"cargo-cp-artifact": "bin/cargo-cp-artifact.js"
}
}
},
"dependencies": {
"cargo-cp-artifact": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.9.tgz",
"integrity": "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA==",
"dev": true
}
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "verifier",
"version": "0.1.0",
"description": "",
"main": "index.node",
"scripts": {
"build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics",
"build-debug": "npm run build --",
"build-release": "npm run build -- --release",
"install": "npm run build-release",
"test": "cargo test"
},
"author": "",
"license": "ISC",
"devDependencies": {
"cargo-cp-artifact": "^0.1"
}
}

View File

@@ -0,0 +1,76 @@
use std::{str, time::Duration};
use elliptic_curve::pkcs8::DecodePublicKey;
use tlsn_core::proof::{SessionProof, TlsProof};
use neon::prelude::*;
use hex::{decode};
fn verify(mut cx: FunctionContext) -> JsResult<JsObject> {
// Deserialize the proof
let proof = cx.argument::<JsString>(0)?.value(&mut cx);
let bytes: Vec<u8> = hex::decode(proof.as_str()).expect("Decoding failed");
let proof: TlsProof = bincode::deserialize(&bytes).expect("Deserialize failed");
let notaryKeyCx = cx.argument::<JsString>(1)?.value(&mut cx);
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;
// Verify the session proof against the Notary's public key
//
// This verifies the identity of the server using a default certificate verifier which trusts
// the root certificates from the `webpki-roots` crate.
session
.verify_with_default_cert_verifier(notary_pubkey(&notaryKeyCx))
.unwrap();
let SessionProof {
// The session header that was signed by the Notary is a succinct commitment to the TLS transcript.
header,
// This is the session_info, which contains the server_name, that is 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).unwrap();
// Replace the bytes which the Prover chose not to disclose with 'X'
sent.set_redacted(b'X');
recv.set_redacted(b'X');
let obj: Handle<JsObject> = cx.empty_object();
let sentStr = cx.string(String::from_utf8(sent.data().to_vec()).unwrap());
let recvStr = cx.string(String::from_utf8(recv.data().to_vec()).unwrap());
let sessionTime = cx.number(header.time() as f64);
obj.set(&mut cx, "sent", sentStr)?;
obj.set(&mut cx, "recv", recvStr)?;
obj.set(&mut cx, "time", sessionTime)?;
Ok(obj)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("verify", verify)?;
Ok(())
}
/// Returns a Notary pubkey trusted by this Verifier
fn notary_pubkey(key: &str) -> p256::PublicKey {
p256::PublicKey::from_public_key_pem(key).unwrap()
}

View File

@@ -2,7 +2,6 @@ import 'dotenv/config';
import express from 'express';
import fileUpload from 'express-fileupload';
import stream from 'stream';
import path from 'path';
import { addBytes, getCID } from './services/ipfs';
import App from '../web/pages/App';
import { Provider } from 'react-redux';
@@ -12,7 +11,9 @@ import { StaticRouter } from 'react-router-dom/server';
import configureAppStore, { AppRootState } from '../web/store';
// @ts-ignore
import { verify } from '../rs/verifier/index.node';
import { Proof } from 'tlsn-js/build/types';
// @ts-ignore
import { verify as verifyV6 } from '../rs/0.1.0-alpha.6/index.node';
import { Attestation } from '../web/utils/types/types';
const app = express();
const port = 3000;
@@ -68,6 +69,13 @@ app.get('/gateway/ipfs/:cid', async (req, res) => {
app.get('/ipfs/:cid', async (req, res) => {
// If there is no file from CID or JSON cannot be parsed, redirect to root
try {
const { cid } = req.params;
const [, isWasm] = cid.split('.');
if (isWasm) {
return res.redirect(`/${cid}`);
}
const storeConfig: AppRootState = {
notaryKey: { key: '' },
proofUpload: {
@@ -78,7 +86,7 @@ app.get('/ipfs/:cid', async (req, res) => {
};
const file = await getCID(req.params.cid);
const jsonProof: Proof = JSON.parse(file);
const jsonProof: Attestation = JSON.parse(file);
storeConfig.proofs.ipfs[req.params.cid] = {
raw: jsonProof,
@@ -88,13 +96,20 @@ app.get('/ipfs/:cid', async (req, res) => {
* Verify the proof if notary url exist
* redirect to root if verification fails
*/
if (jsonProof.notaryUrl) {
const proof = await verify(
if (!jsonProof.version && jsonProof.notaryUrl) {
const proof = await verifyV6(
file,
await fetchPublicKeyFromNotary(jsonProof.notaryUrl),
);
proof.notaryUrl = jsonProof.notaryUrl;
storeConfig.proofs.ipfs[req.params.cid].proof = proof;
} else if (jsonProof.version === '1.0') {
const proof = await verifyV6(
jsonProof.data,
await fetchPublicKeyFromNotary(jsonProof.meta.notaryUrl),
);
proof.notaryUrl = jsonProof.meta.notaryUrl;
storeConfig.proofs.ipfs[req.params.cid].proof = proof;
}
const store = configureAppStore(storeConfig);
@@ -147,7 +162,42 @@ app.get('/ipfs/:cid', async (req, res) => {
});
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../ui', 'index.html'));
const storeConfig: AppRootState = {
notaryKey: { key: '' },
proofUpload: {
proofs: [],
selectedProof: null,
},
proofs: { ipfs: {} },
};
const store = configureAppStore(storeConfig);
const html = renderToString(
<Provider store={store}>
<StaticRouter location={req.url}>
<App />
</StaticRouter>
</Provider>,
);
const preloadedState = store.getState();
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TLSNotary Explorer</title>
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};
</script>
<script defer src="/index.bundle.js"></script>
</head>
<body>
<div id="root">${html}</div>
<div id="modal-root"></div>
</body>
</html>
`);
});
app.listen(port, () => {

View File

@@ -1,45 +0,0 @@
.button {
@apply bg-slate-100;
@apply text-slate-500;
@apply font-bold;
@apply px-2 py-0.5;
user-select: none;
&:hover {
@apply text-slate-600;
@apply bg-slate-200;
}
&:active {
@apply text-slate-700;
@apply bg-slate-300;
}
&--primary {
@apply bg-primary/[0.8];
@apply text-white;
&:hover {
@apply bg-primary/[0.9];
@apply text-white;
}
&:active {
@apply bg-primary;
@apply text-white;
}
}
&:disabled {
@apply opacity-50;
@apply select-none;
&:hover {
@apply text-slate-400;
}
&:active {
@apply text-slate-400;
}
}
}

View File

@@ -1,89 +0,0 @@
import React, { FormEvent, ReactElement, useState } from 'react';
import { useDispatch } from 'react-redux';
import { setKey } from '../../store/notaryKey';
import keys from '../../utils/keys.json';
export default function NotaryKey(): ReactElement {
const dispatch = useDispatch();
const defaultKey: string = keys.defaultKey;
const notaryPseKey: string = keys.notaryPseKey;
const [notaryKey, setNotaryKey] = useState<string>(notaryPseKey);
const [errors, setError] = useState<string | null>(null);
const isValidPEMKey = (key: string): boolean => {
try {
const trimmedKey = key.trim();
if (
!trimmedKey.startsWith('-----BEGIN PUBLIC KEY-----') ||
!trimmedKey.endsWith('-----END PUBLIC KEY-----')
) {
setError('Invalid PEM format: header or footer missing');
return false;
}
const keyContent = trimmedKey
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.trim();
try {
atob(keyContent);
} catch (err) {
setError('Invalid Base64 encoding');
return false;
}
return true;
} catch (err) {
console.error('Error validating key:', err);
return false;
}
};
const handleInput = (
e:
| FormEvent<HTMLTextAreaElement>
| React.MouseEvent<HTMLButtonElement, MouseEvent>,
key?: string | undefined,
) => {
setError(null);
const keyInput =
key !== undefined
? key
: e.currentTarget instanceof HTMLTextAreaElement
? e.currentTarget.value
: '';
if (isValidPEMKey(keyInput)) {
setNotaryKey(keyInput);
dispatch(setKey(keyInput));
} else {
setNotaryKey(keyInput);
}
};
return (
<details className="w-3/4 mx-auto">
<summary className="text-2xl font-bold cursor-pointer">
Change Notary Public Key:
</summary>
<textarea
className="w-full h-40 rounded bg-gray-800 text-white resize-none mt-4 p-4"
value={notaryKey}
onChange={(e) => handleInput(e)}
/>
{errors && <p className="text-red-500 mt-2">{errors}</p>}
<div className="flex mt-4">
<button
className="button"
onClick={(e) => handleInput(e, notaryPseKey)}
>
notary.pse.dev
</button>
<button className="button" onClick={(e) => handleInput(e, defaultKey)}>
Default
</button>
</div>
</details>
);
}

View File

@@ -7,8 +7,7 @@ import React, {
} from 'react';
import c from 'classnames';
import classNames from 'classnames';
import { Proof as VerifiedProof } from '../../utils/types/types';
import { Proof } from 'tlsn-js/build/types';
import { Attestation, Proof as VerifiedProof } from '../../utils/types/types';
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
import Icon from '../Icon';
import { useDispatch } from 'react-redux';
@@ -20,7 +19,7 @@ import copy from 'copy-to-clipboard';
export default function ProofViewer(props: {
file: File;
verifiedProof: VerifiedProof;
proof: Proof;
proof: Attestation;
className?: string;
}): ReactElement {
const [tab, setTab] = useState('sent');
@@ -56,6 +55,9 @@ export default function ProofViewer(props: {
)}
<div className="flex flex-col px-2">
<div className="flex flex-row gap-2 items-center">
<TabLabel onClick={() => setTab('info')} active={tab === 'info'}>
Info
</TabLabel>
<TabLabel onClick={() => setTab('sent')} active={tab === 'sent'}>
Sent
</TabLabel>
@@ -70,17 +72,31 @@ export default function ProofViewer(props: {
</div>
</div>
<div className="flex flex-col flex-grow px-2">
{tab === 'info' && (
<div className="w-full bg-slate-100 text-slate-800 border p-2 text-xs break-all h-full outline-none font-mono">
<div>
<div>Notary URL:</div>
<div>
{props.proof.version === '1.0'
? props.proof.meta.notaryUrl
: props.proof.notaryUrl}
</div>
</div>
</div>
)}
{tab === 'sent' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-xs break-all h-full outline-none font-mono"
value={props.verifiedProof.sent}
></textarea>
readOnly
/>
)}
{tab === 'recv' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-xs break-all h-full outline-none font-mono"
value={props.verifiedProof.recv}
></textarea>
readOnly
/>
)}
</div>
</div>

View File

@@ -7,6 +7,7 @@ import { FileDropdown } from '../FileDropdown';
import { PubkeyInput } from '../../pages/PubkeyInput';
import { Proof } from '../../utils/types/types';
import { File } from '@web-std/file';
import { verify } from '../../utils';
export default function SharedProof(): ReactElement {
const { cid } = useParams();
@@ -32,8 +33,6 @@ export default function SharedProof(): ReactElement {
const onVerify = useCallback(
async (key = '') => {
if (!proofData?.raw) return;
const { verify } = await import('tlsn-js/src/index');
const resp = await verify(proofData?.raw, key);
setVerifiedProof(resp);
},
@@ -51,15 +50,19 @@ export default function SharedProof(): ReactElement {
onDelete={() => navigate('/')}
/>
)}
{!!proofData.raw && !verifiedProof && (
<PubkeyInput className="w-2/3 flex-shrink-0" onNext={onVerify} />
{!!proofData.raw && !verifiedProof && !proofData.proof && (
<PubkeyInput
className="w-2/3 flex-shrink-0"
onNext={onVerify}
proof={proofData.raw}
/>
)}
{verifiedProof && (
{(verifiedProof || proofData.proof) && (
<ProofViewer
className="h-4/5 w-2/3 flex-shrink-0"
file={file}
proof={proofData.raw}
verifiedProof={verifiedProof}
verifiedProof={verifiedProof || proofData.proof!}
/>
)}
</div>

View File

@@ -1,6 +1,6 @@
import 'isomorphic-fetch';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { hydrateRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import type {} from 'redux-thunk/extend-redux';
@@ -13,13 +13,13 @@ const store = configureAppStore(window.__PRELOADED_STATE__);
delete window.__PRELOADED_STATE__;
(async () => {
ReactDOM.hydrate(
hydrateRoot(
document.getElementById('root')!,
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
})();

View File

@@ -1,15 +1,15 @@
import React, { ReactElement, useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { readFileAsync, safeParseJSON } from '../../utils';
import { readFileAsync, safeParseJSON, verify } from '../../utils';
import FileUploadInput from '../../components/FileUploadInput';
import ProofViewer from '../../components/ProofViewer';
import { Proof as VerifiedProof } from '../../utils/types/types';
import { Attestation, Proof as VerifiedProof } from '../../utils/types/types';
import { FileDropdown } from '../../components/FileDropdown';
import { PubkeyInput } from '../PubkeyInput';
export default function FileDrop(): ReactElement {
const dispatch = useDispatch();
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<string>('');
const [verifiedProof, setVerifiedProof] = useState<VerifiedProof | null>(
null,
);
@@ -19,13 +19,19 @@ export default function FileDrop(): ReactElement {
const [pubkey, setPubkey] = useState('');
const [uploading, setUploading] = useState(false);
const onVerify = useCallback(async (json: any, key = '') => {
const { verify } = await import('tlsn-js');
const onVerify = useCallback(async (json: Attestation, key = '') => {
try {
const resp = await verify(json, key);
setVerifiedProof(resp);
setStep('result');
} catch (e) {
} catch (e: any) {
if (e?.message !== 'Failed to fetch') {
setError(
typeof e === 'string'
? e
: e?.message || 'Unknown Verification Error.',
);
}
setStep('pubkey');
}
}, []);
@@ -42,10 +48,10 @@ export default function FileDrop(): ReactElement {
return;
}
setError(null);
setError('');
const proofContent = await readFileAsync(file);
const json = safeParseJSON(proofContent);
const json: Attestation = safeParseJSON(proofContent);
if (!json) {
setError(proofContent || 'Invalid proof');
@@ -54,11 +60,11 @@ export default function FileDrop(): ReactElement {
setRawJson(json);
if (!json?.notaryUrl) {
setStep('pubkey');
setFile(file);
return;
}
// if (!json?.notaryUrl) {
// setStep('pubkey');
// setFile(file);
// return;
// }
try {
await onVerify(json);
@@ -113,6 +119,9 @@ export default function FileDrop(): ReactElement {
}}
/>
)}
{error && (
<span className="text-red-500 text-sm w-2/3 text-center">{error}</span>
)}
{(() => {
switch (step) {
case 'upload':
@@ -127,6 +136,8 @@ export default function FileDrop(): ReactElement {
return (
<PubkeyInput
className="w-2/3 flex-shrink-0"
proof={rawJson}
setError={setError}
onNext={onPubkeyChange}
/>
);
@@ -143,7 +154,6 @@ export default function FileDrop(): ReactElement {
return null;
}
})()}
{error && <span className="text-red-500 text-sm">{error}</span>}
</div>
);
}

View File

@@ -1,10 +1,14 @@
import React, { ChangeEvent, useCallback, useState } from 'react';
import classNames from 'classnames';
import { Attestation } from '../../utils/types/types';
export function PubkeyInput(props: {
onNext: (pubkey: string) => Promise<void>;
proof: Attestation;
setError?: (msg: string) => void;
className?: string;
}) {
const { proof } = props;
const [error, setError] = useState('');
const [pubkey, setPubkey] = useState('');
@@ -39,8 +43,9 @@ export function PubkeyInput(props: {
const onChange = useCallback(
async (e: ChangeEvent<HTMLTextAreaElement>) => {
props.setError && props.setError('');
setError('');
const pubkey = e.target.value;
const pubkey = e.target.value.replace(/\\n/g, '\n');
setPubkey(pubkey);
},
[pubkey],
@@ -62,11 +67,18 @@ export function PubkeyInput(props: {
return (
<div className={classNames('flex flex-col gap-2', props.className)}>
<div className="font-semibold">Please enter the notary key:</div>
<div className="font-semibold text-sm cursor-default">
Please enter the notary key for{' '}
<span className="text-blue-500 italic font-normal">
{proof.version === '1.0' ? proof.meta.notaryUrl : proof.notaryUrl}
</span>
:
</div>
<textarea
className="outline-0 flex-grow w-full bg-slate-100 rouned-xs !border border-slate-300 focus-within:border-slate-500 resize-none p-2 h-[24rem]"
className="outline-0 flex-grow w-full bg-slate-100 rouned-xs !border border-slate-300 focus-within:border-slate-500 resize-none p-2 h-[24rem] font-mono text-xs"
onChange={onChange}
placeholder={`-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----`}
value={pubkey}
/>
<div className="flex flex-row justify-end gap-2 items-center">
{error && <span className="text-red-500 text-sm">{error}</span>}

View File

@@ -1,9 +1,11 @@
import { ThunkDispatch } from 'redux-thunk';
import { AppRootState } from './index';
import type { Proof } from 'tlsn-js/build/types';
import type { Proof } from 'tlsn-js-v5/build/types';
import { useSelector } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { EXPLORER_URL } from '../utils/constants';
import { Attestation } from '../utils/types/types';
import { verify } from '../utils';
enum ActionType {
SetIPFSProof = 'proofs/setIPFSProof',
@@ -17,7 +19,7 @@ export type Action<payload = any> = {
};
type ProofData = {
raw: Proof;
raw: Attestation;
proof?: {
time: number;
sent: string;
@@ -58,8 +60,6 @@ export const fetchProofFromIPFS =
data = old.raw;
}
const { verify } = await import('tlsn-js/src');
const proof = await verify(data, notaryKey);
dispatch(setIPFSProof({ cid, proof, raw: data }));

View File

@@ -1,4 +1,5 @@
import React, { ReactElement, useRef } from 'react';
import { Attestation, Proof } from './types/types';
export const readFileAsync = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
@@ -82,3 +83,68 @@ export function download(filename: string, content: string) {
document.body.removeChild(element);
}
let tlsnInitPromise: Promise<any> | null = null;
async function initTlsnJs() {
if (tlsnInitPromise) return tlsnInitPromise;
const { promise, resolve } = defer();
tlsnInitPromise = promise;
const { default: init } = await import('tlsn-js');
await init();
resolve();
}
export async function verify(
attestation: Attestation,
pubKey: string,
): Promise<Proof> {
let key = pubKey;
const { NotaryServer } = await import('tlsn-js');
switch (attestation.version) {
case undefined: {
const { verify } = await import('tlsn-js-v5');
key = key || (await NotaryServer.from(attestation.notaryUrl).publicKey());
return await verify(attestation, key);
}
case '1.0': {
const { TlsProof } = await import('tlsn-js');
console.log(Buffer.from(attestation.data, 'hex').toString('binary'));
await initTlsnJs();
key =
key ||
(await NotaryServer.from(attestation.meta.notaryUrl).publicKey());
const tlsProof = new TlsProof(attestation.data);
const data = await tlsProof.verify({
typ: 'P256',
key: key,
});
return {
sent: data.sent,
recv: data.recv,
time: data.time,
notaryUrl: attestation.meta.notaryUrl,
};
}
}
throw new Error('Invalid Proof');
}
function defer(): {
promise: Promise<any>;
resolve: (args?: any) => any;
reject: (args?: any) => any;
} {
let resolve: any, reject: any;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve: resolve as (args?: any) => any,
reject: reject as (args?: any) => any,
};
}

View File

@@ -4,3 +4,22 @@ export interface Proof {
recv: string;
notaryUrl: string;
}
export type AttestationV0 = {
version?: undefined;
session: any;
substrings: any;
notaryUrl: string;
};
export type AttestationV1 = {
version: '1.0';
data: string;
meta: {
notaryUrl: string;
websocketProxyUrl: string;
pluginUrl?: string;
};
};
export type Attestation = AttestationV0 | AttestationV1;

9
web/utils/worker.ts Normal file
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

@@ -69,7 +69,7 @@ const options = {
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules\/(?!(tlsn-js)\/).*/,
exclude: /node_modules\/(?!(tlsn-js|tlsn-js-v5)\/).*/,
use: [
{
loader: require.resolve("ts-loader"),

View File

@@ -82,7 +82,7 @@ var options = {
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules\/(?!(tlsn-js)\/).*/,
exclude: /node_modules\/(?!(tlsn-js|tlsn-js-v5)\/).*/,
use: [
{
loader: require.resolve("ts-loader"),
@@ -145,6 +145,11 @@ var options = {
new webpack.EnvironmentPlugin(["NODE_ENV"]),
new CopyWebpackPlugin({
patterns: [
{
from: "node_modules/tlsn-js-v5/build",
to: path.join(__dirname, "build", "ui"),
force: true,
},
{
from: "node_modules/tlsn-js/build",
to: path.join(__dirname, "build", "ui"),
@@ -157,12 +162,12 @@ var options = {
},
]
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, "static", "index.html"),
filename: "index.html",
chunks: ["index"],
cache: false,
}),
// new HtmlWebpackPlugin({
// template: path.join(__dirname, "static", "index.html"),
// filename: "index.html",
// chunks: ["index"],
// cache: false,
// }),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),