mirror of
https://github.com/tlsnotary/explorer.git
synced 2026-01-09 06:48:09 -05:00
feat: integrate with v0.1.0-alpha.6 (#27)
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,4 +4,4 @@
|
||||
.idea
|
||||
build
|
||||
.env
|
||||
rs/verifier/target
|
||||
rs/**/target
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
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
1526
rs/0.1.0-alpha.6/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
rs/0.1.0-alpha.6/Cargo.toml
Normal file
22
rs/0.1.0-alpha.6/Cargo.toml
Normal 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
119
rs/0.1.0-alpha.6/README.md
Normal 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
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
34
rs/0.1.0-alpha.6/package-lock.json
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
18
rs/0.1.0-alpha.6/package.json
Normal file
18
rs/0.1.0-alpha.6/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
76
rs/0.1.0-alpha.6/src/lib.rs
Normal file
76
rs/0.1.0-alpha.6/src/lib.rs
Normal 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(¬aryKeyCx))
|
||||
.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()
|
||||
}
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
})();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
9
web/utils/worker.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import init, { Prover, NotarizedSession, TlsProof } from 'tlsn-js';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Prover,
|
||||
NotarizedSession,
|
||||
TlsProof,
|
||||
});
|
||||
@@ -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"),
|
||||
|
||||
@@ -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'],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user