feat(tlsn-wasm): wasm bindings (#536)

* feat(tlsn-wasm): wasm bindings

* fix wasm ci

* clippy

* clippy allow

* add build config and remove redundant to_vec
This commit is contained in:
sinu.eth
2024-07-25 14:15:11 +09:00
committed by GitHub
parent 040608bb6e
commit 2205cb3b2c
35 changed files with 1566 additions and 14 deletions

View File

@@ -59,7 +59,7 @@ jobs:
- name: Test
run: cargo test
build-wasm:
name: Build for target wasm32-unknown-unknown
name: Build and test wasm
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -71,11 +71,30 @@ jobs:
targets: wasm32-unknown-unknown
toolchain: stable
- name: Install nightly rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown,x86_64-unknown-linux-gnu
toolchain: nightly
components: rust-src
- name: Install chromedriver
run: |
sudo apt-get update
sudo apt-get install -y chromium-chromedriver
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Use caching
uses: Swatinem/rust-cache@v2.5.0
with:
workspaces: crates/wasm-test-runner -> ../target
- name: Build
run: cargo build -p tlsn-tls-client -p tlsn-tls-core -p tlsn-core --target wasm32-unknown-unknown
- name: Run tests
run: |
cd crates/wasm-test-runner
RUST_LOG=debug ./run.sh
tests-integration:
name: Run tests release build
runs-on: ubuntu-latest

View File

@@ -23,7 +23,9 @@ members = [
"crates/tls/core",
"crates/tls/mpc",
"crates/tls/server-fixture",
"crates/wasm-test-runner",
"crates/verifier",
"crates/wasm",
]
resolver = "2"
@@ -97,8 +99,10 @@ http-body-util = { version = "0.1" }
hyper = { version = "1.1" }
hyper-util = { version = "0.1" }
log = { version = "0.4" }
once_cell = { version = "1.19" }
opaque-debug = { version = "0.3" }
p256 = { version = "0.13" }
pin-project-lite = { version = "0.2" }
rand = { version = "0.8" }
rand_chacha = { version = "0.3" }
rand_core = { version = "0.6" }

View File

@@ -32,6 +32,11 @@ impl ProverConfig {
ProverConfigBuilder::default()
}
/// Returns the instance id.
pub fn id(&self) -> &str {
&self.id
}
/// Returns the maximum number of bytes that can be sent.
pub fn max_sent_data(&self) -> usize {
self.max_sent_data

View File

@@ -38,12 +38,13 @@ use uid_mux::FramedUidMux as _;
#[cfg(feature = "formats")]
use crate::http::{state as http_state, HttpProver, HttpProverError};
use tracing::{debug, debug_span, instrument, Instrument};
use tracing::{debug, info_span, instrument, Instrument, Span};
/// A prover instance.
#[derive(Debug)]
pub struct Prover<T: state::ProverState> {
config: ProverConfig,
span: Span,
state: T,
}
@@ -54,8 +55,10 @@ impl Prover<state::Initialized> {
///
/// * `config` - The configuration for the prover.
pub fn new(config: ProverConfig) -> Self {
let span = info_span!("prover", id = config.id());
Self {
config,
span,
state: state::Initialized,
}
}
@@ -68,7 +71,7 @@ impl Prover<state::Initialized> {
/// # Arguments
///
/// * `socket` - The socket to the TLS verifier.
#[instrument(level = "debug", skip_all, err)]
#[instrument(parent = &self.span, level = "debug", skip_all, err)]
pub async fn setup<S: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
self,
socket: S,
@@ -97,6 +100,7 @@ impl Prover<state::Initialized> {
Ok(Prover {
config: self.config,
span: self.span,
state: state::Setup {
io,
mux_ctrl,
@@ -119,7 +123,7 @@ impl Prover<state::Setup> {
/// # Arguments
///
/// * `socket` - The socket to the server.
#[instrument(level = "debug", skip_all, err)]
#[instrument(parent = &self.span, level = "debug", skip_all, err)]
pub async fn connect<S: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
self,
socket: S,
@@ -149,6 +153,7 @@ impl Prover<state::Setup> {
let start_time = web_time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let fut = Box::pin({
let span = self.span.clone();
let mpc_ctrl = mpc_ctrl.clone();
async move {
let conn_fut = async {
@@ -168,6 +173,7 @@ impl Prover<state::Setup> {
Ok(Prover {
config: self.config,
span: self.span,
state: state::Closed {
io,
mux_ctrl,
@@ -185,7 +191,7 @@ impl Prover<state::Setup> {
},
})
}
.instrument(debug_span!("prover"))
.instrument(span)
});
Ok((
@@ -222,6 +228,7 @@ impl Prover<state::Closed> {
pub fn start_notarize(self) -> Prover<Notarize> {
Prover {
config: self.config,
span: self.span,
state: self.state.into(),
}
}
@@ -233,6 +240,7 @@ impl Prover<state::Closed> {
pub fn start_prove(self) -> Prover<Prove> {
Prover {
config: self.config,
span: self.span,
state: self.state.into(),
}
}
@@ -245,6 +253,8 @@ async fn setup_mpc_backend(
mux: &MuxControl,
exec: &mut Executor,
) -> Result<(MpcTlsLeader, DEAPThread, OTReceiver), ProverError> {
debug!("starting MPC backend setup");
let mut ot_sender = kos::Sender::new(
config.build_ot_sender_config(),
chou_orlandi::Receiver::new(config.build_base_ot_receiver_config()),

View File

@@ -28,7 +28,7 @@ impl Prover<Notarize> {
}
/// Finalizes the notarization returning a [`NotarizedSession`].
#[instrument(level = "debug", skip_all, err)]
#[instrument(parent = &self.span, level = "debug", skip_all, err)]
pub async fn finalize(self) -> Result<NotarizedSession, ProverError> {
let Notarize {
mut io,

View File

@@ -60,7 +60,7 @@ impl Prover<ProveState> {
}
/// Prove transcript values
#[instrument(level = "debug", skip_all, err)]
#[instrument(parent = &self.span, level = "debug", skip_all, err)]
pub async fn prove(&mut self) -> Result<(), ProverError> {
let mut proving_info = std::mem::take(&mut self.state.proving_info);
@@ -127,7 +127,7 @@ impl Prover<ProveState> {
}
/// Finalize the proving
#[instrument(level = "debug", skip_all, err)]
#[instrument(parent = &self.span, level = "debug", skip_all, err)]
pub async fn finalize(self) -> Result<(), ProverError> {
let ProveState {
mut io,

View File

@@ -38,3 +38,4 @@ rand = { workspace = true }
signature = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
web-time = { workspace = true }

View File

@@ -12,7 +12,7 @@ use mpz_common::Allocate;
use serio::StreamExt;
use uid_mux::FramedUidMux;
use std::time::{SystemTime, UNIX_EPOCH};
use web_time::{SystemTime, UNIX_EPOCH};
use futures::{AsyncRead, AsyncWrite, TryFutureExt};
use mpz_garble::config::Role as DEAPRole;
@@ -27,19 +27,22 @@ use tlsn_common::{
};
use tlsn_core::{proof::SessionInfo, RedactedTranscript, SessionHeader, Signature};
use tracing::{debug, info, instrument};
use tracing::{debug, info, info_span, instrument, Span};
/// A Verifier instance.
pub struct Verifier<T: state::VerifierState> {
config: VerifierConfig,
span: Span,
state: T,
}
impl Verifier<state::Initialized> {
/// Creates a new verifier.
pub fn new(config: VerifierConfig) -> Self {
let span = info_span!("verifier", id = config.id());
Self {
config,
span,
state: state::Initialized,
}
}
@@ -51,6 +54,7 @@ impl Verifier<state::Initialized> {
/// # Arguments
///
/// * `socket` - The socket to the prover.
#[instrument(parent = &self.span, level = "info", skip_all, err)]
pub async fn setup<S: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
self,
socket: S,
@@ -85,6 +89,7 @@ impl Verifier<state::Initialized> {
Ok(Verifier {
config: self.config,
span: self.span,
state: state::Setup {
io,
mux_ctrl,
@@ -106,6 +111,7 @@ impl Verifier<state::Initialized> {
///
/// * `socket` - The socket to the prover.
/// * `signer` - The signer used to sign the notarization result.
#[instrument(parent = &self.span, level = "info", skip_all, err)]
pub async fn notarize<S: AsyncWrite + AsyncRead + Send + Unpin + 'static, T>(
self,
socket: S,
@@ -130,6 +136,7 @@ impl Verifier<state::Initialized> {
/// # Arguments
///
/// * `socket` - The socket to the prover.
#[instrument(parent = &self.span, level = "info", skip_all, err)]
pub async fn verify<S: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
self,
socket: S,
@@ -144,6 +151,7 @@ impl Verifier<state::Initialized> {
impl Verifier<state::Setup> {
/// Runs the verifier until the TLS connection is closed.
#[instrument(parent = &self.span, level = "info", skip_all, err)]
pub async fn run(self) -> Result<Verifier<state::Closed>, VerifierError> {
let state::Setup {
io,
@@ -177,6 +185,7 @@ impl Verifier<state::Setup> {
Ok(Verifier {
config: self.config,
span: self.span,
state: state::Closed {
io,
mux_ctrl,
@@ -203,6 +212,7 @@ impl Verifier<state::Closed> {
pub fn start_notarize(self) -> Verifier<Notarize> {
Verifier {
config: self.config,
span: self.span,
state: self.state.into(),
}
}
@@ -214,6 +224,7 @@ impl Verifier<state::Closed> {
pub fn start_verify(self) -> Verifier<Verify> {
Verifier {
config: self.config,
span: self.span,
state: self.state.into(),
}
}
@@ -227,6 +238,8 @@ async fn setup_mpc_backend(
exec: &mut Executor,
encoder_seed: [u8; 32],
) -> Result<(MpcTlsFollower, DEAPThread, OTSender), VerifierError> {
debug!("starting MPC backend setup");
let mut ot_sender = kos::Sender::new(
config.build_ot_sender_config(),
chou_orlandi::Receiver::new(config.build_base_ot_receiver_config()),

View File

@@ -19,7 +19,7 @@ impl Verifier<Notarize> {
/// # Arguments
///
/// * `signer` - The signer used to sign the notarization result.
#[instrument(level = "debug", skip_all, err)]
#[instrument(parent = &self.span, level = "debug", skip_all, err)]
pub async fn finalize<T>(self, signer: &impl Signer<T>) -> Result<SessionHeader, VerifierError>
where
T: Into<Signature>,

View File

@@ -12,7 +12,7 @@ use tlsn_core::{
RedactedTranscript, TranscriptSlice,
};
use tracing::info;
use tracing::{info, instrument};
impl Verifier<VerifyState> {
/// Receives the **purported** transcript from the Prover.
@@ -20,6 +20,7 @@ impl Verifier<VerifyState> {
/// # Warning
///
/// The content of the received transcripts can not be considered authentic until after finalization.
#[instrument(parent = &self.span, level = "info", skip_all, err)]
pub async fn receive(
&mut self,
) -> Result<(RedactedTranscript, RedactedTranscript), VerifierError> {
@@ -110,6 +111,7 @@ impl Verifier<VerifyState> {
}
/// Verifies the TLS session.
#[instrument(parent = &self.span, level = "info", skip_all, err)]
pub async fn finalize(self) -> Result<SessionInfo, VerifierError> {
let VerifyState {
mut io,

View File

@@ -0,0 +1,30 @@
[package]
name = "tlsn-wasm-test-runner"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
tlsn-core = { workspace = true }
tlsn-prover = { workspace = true }
tlsn-server-fixture = { workspace = true }
tlsn-tls-core = { workspace = true }
tlsn-verifier = { workspace = true }
websocket-relay = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "73a6be1" }
anyhow = { workspace = true }
axum = { workspace = true }
chromiumoxide = { version = "0.6", features = ["tokio-runtime"] }
futures = { workspace = true }
once_cell = { workspace = true }
p256 = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full"] }
tokio-tungstenite = { version = "0.23", features = ["url"] }
tokio-util = { workspace = true, features = ["compat"] }
tower = { version = "0.4" }
tower-http = { version = "0.5", features = ["fs", "set-header"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }

4
crates/wasm-test-runner/run.sh Executable file
View File

@@ -0,0 +1,4 @@
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \
rustup run nightly \
wasm-pack build ../wasm --target web --no-pack --out-dir=../wasm-test-runner/static/generated -- -Zbuild-std=panic_abort,std --features test \
&& cargo run --release

View File

@@ -0,0 +1,86 @@
use anyhow::{anyhow, Result};
use chromiumoxide::{
cdp::{
browser_protocol::log::{EventEntryAdded, LogEntryLevel},
js_protocol::runtime::EventExceptionThrown,
},
Browser, BrowserConfig, Page,
};
use futures::{Future, FutureExt, StreamExt};
use std::env;
use tracing::{debug, error, instrument};
use crate::{TestResult, DEFAULT_SERVER_IP, DEFAULT_WASM_PORT};
#[instrument]
pub async fn run() -> Result<Vec<TestResult>> {
let config = BrowserConfig::builder().build().map_err(|s| anyhow!(s))?;
debug!("launching chromedriver");
let (mut browser, mut handler) = Browser::launch(config).await?;
debug!("chromedriver started");
tokio::spawn(async move {
while let Some(res) = handler.next().await {
res.unwrap();
}
});
let wasm_port: u16 = env::var("WASM_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_WASM_PORT);
let wasm_addr: String = env::var("WASM_IP").unwrap_or_else(|_| DEFAULT_SERVER_IP.to_string());
let page = browser
.new_page(&format!("http://{}:{}/index.html", wasm_addr, wasm_port))
.await?;
tokio::spawn(register_listeners(&page).await?);
page.wait_for_navigation().await?;
let results: Vec<TestResult> = page
.evaluate(
r#"
(async () => {
await window.testWorker.init();
return await window.testWorker.run();
})();
"#,
)
.await?
.into_value()?;
browser.close().await?;
browser.wait().await?;
Ok(results)
}
async fn register_listeners(page: &Page) -> Result<impl Future<Output = ()>> {
let mut logs = page.event_listener::<EventEntryAdded>().await?.fuse();
let mut exceptions = page.event_listener::<EventExceptionThrown>().await?.fuse();
Ok(futures::future::join(
async move {
while let Some(event) = logs.next().await {
let entry = &event.entry;
match entry.level {
LogEntryLevel::Error => {
error!("{:?}", entry);
}
_ => {
debug!("{:?}: {}", entry.timestamp, entry.text);
}
}
}
},
async move {
while let Some(event) = exceptions.next().await {
error!("{:?}", event);
}
},
)
.map(|_| ()))
}

View File

@@ -0,0 +1,37 @@
use std::fmt::Display;
pub mod chrome_driver;
pub mod server_fixture;
pub mod tlsn_fixture;
pub mod wasm_server;
pub mod ws;
pub static DEFAULT_SERVER_IP: &str = "127.0.0.1";
pub static DEFAULT_WASM_PORT: u16 = 8013;
pub static DEFAULT_WS_PORT: u16 = 8080;
pub static DEFAULT_SERVER_PORT: u16 = 8083;
pub static DEFAULT_VERIFIER_PORT: u16 = 8010;
pub static DEFAULT_NOTARY_PORT: u16 = 8011;
pub static DEFAULT_PROVER_PORT: u16 = 8012;
#[derive(Debug, serde::Deserialize)]
pub struct TestResult {
pub name: String,
pub passed: bool,
pub error: Option<String>,
}
impl Display for TestResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.passed {
write!(f, "{}: passed", self.name)?;
} else {
write!(f, "{}: failed", self.name)?;
if let Some(error) = &self.error {
write!(f, "\ncaused by: {}", error)?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,42 @@
use anyhow::Result;
fn init_tracing() {
use tracing_subscriber::EnvFilter;
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
}
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
let fut_wasm = tlsn_wasm_test_runner::wasm_server::start().await?;
let fut_proxy = tlsn_wasm_test_runner::ws::start().await?;
let fut_tlsn = tlsn_wasm_test_runner::tlsn_fixture::start().await?;
let fut_server = tlsn_wasm_test_runner::server_fixture::start().await?;
tokio::spawn(async move {
futures::future::try_join4(fut_wasm, fut_proxy, fut_tlsn, fut_server)
.await
.unwrap()
});
let results = tlsn_wasm_test_runner::chrome_driver::run().await?;
for result in &results {
println!("{}", result);
}
let passed = results.iter().filter(|r| r.passed).count();
let failed = results.iter().filter(|r| !r.passed).count();
println!("{} passed, {} failed", passed, failed);
if results.iter().any(|r| !r.passed) {
std::process::exit(1);
}
Ok(())
}

View File

@@ -0,0 +1,32 @@
use std::{env, net::IpAddr};
use anyhow::Result;
use futures::Future;
use tokio::net::TcpListener;
use tokio_util::compat::TokioAsyncReadCompatExt;
use tracing::{info, instrument};
use crate::{DEFAULT_SERVER_IP, DEFAULT_SERVER_PORT};
#[instrument]
pub async fn start() -> Result<impl Future<Output = Result<()>>> {
let port: u16 = env::var("SERVER_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_SERVER_PORT);
let addr: IpAddr = env::var("SERVER_IP")
.map(|addr| addr.parse().expect("should be valid IP address"))
.unwrap_or(IpAddr::V4(DEFAULT_SERVER_IP.parse().unwrap()));
let listener = TcpListener::bind((addr, port)).await?;
info!("listening on: {}", listener.local_addr()?);
Ok(async move {
loop {
let (socket, addr) = listener.accept().await?;
info!("accepted connection from: {}", addr);
tokio::spawn(tlsn_server_fixture::bind(socket.compat()));
}
})
}

View File

@@ -0,0 +1,166 @@
use std::{env, net::IpAddr};
use anyhow::Result;
use futures::{AsyncReadExt, AsyncWriteExt, Future};
use tls_core::{anchors::RootCertStore, verify::WebPkiVerifier};
use tlsn_core::Direction;
use tlsn_prover::tls::{Prover, ProverConfig};
use tlsn_server_fixture::{CA_CERT_DER, SERVER_DOMAIN};
use tlsn_verifier::tls::{Verifier, VerifierConfig};
use tokio::net::{TcpListener, TcpStream};
use tokio_util::compat::TokioAsyncReadCompatExt;
use tracing::{info, instrument};
use crate::{
DEFAULT_NOTARY_PORT, DEFAULT_PROVER_PORT, DEFAULT_SERVER_IP, DEFAULT_SERVER_PORT,
DEFAULT_VERIFIER_PORT,
};
#[instrument]
pub async fn start() -> Result<impl Future<Output = Result<()>>> {
let verifier_port: u16 = env::var("VERIFIER_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_VERIFIER_PORT);
let notary_port: u16 = env::var("NOTARY_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_NOTARY_PORT);
let prover_port: u16 = env::var("PROVER_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_PROVER_PORT);
let addr: IpAddr = env::var("TLSN_IP")
.map(|addr| addr.parse().expect("should be valid IP address"))
.unwrap_or(IpAddr::V4(DEFAULT_SERVER_IP.parse().unwrap()));
let verifier_listener = TcpListener::bind((addr, verifier_port)).await?;
let notary_listener = TcpListener::bind((addr, notary_port)).await?;
let prover_listener = TcpListener::bind((addr, prover_port)).await?;
Ok(async move {
loop {
tokio::select! {
res = verifier_listener.accept() => {
let (socket, addr) = res?;
info!("verifier accepted connection from: {}", addr);
tokio::spawn(handle_verifier(socket));
},
res = notary_listener.accept() => {
let (socket, addr) = res?;
info!("notary accepted connection from: {}", addr);
tokio::spawn(handle_notary(socket));
},
res = prover_listener.accept() => {
let (socket, addr) = res?;
info!("prover accepted connection from: {}", addr);
tokio::spawn(handle_prover(socket));
},
}
}
})
}
#[instrument(level = "debug", skip_all, err)]
async fn handle_verifier(io: TcpStream) -> Result<()> {
let mut root_store = RootCertStore::empty();
root_store
.add(&tls_core::key::Certificate(
tlsn_server_fixture::CA_CERT_DER.to_vec(),
))
.unwrap();
let config = VerifierConfig::builder()
.id("test")
.max_sent_data(1024)
.max_recv_data(1024)
.cert_verifier(WebPkiVerifier::new(root_store, None))
.build()
.unwrap();
let verifier = Verifier::new(config);
verifier.verify(io.compat()).await?;
Ok(())
}
#[instrument(level = "debug", skip_all, err)]
async fn handle_notary(io: TcpStream) -> Result<()> {
let config = VerifierConfig::builder()
.id("test")
.max_sent_data(1024)
.max_recv_data(1024)
.build()
.unwrap();
let verifier = Verifier::new(config);
let signing_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng());
verifier
.notarize::<_, p256::ecdsa::Signature>(io.compat(), &signing_key)
.await?;
Ok(())
}
#[instrument(level = "debug", skip_all, err)]
async fn handle_prover(io: TcpStream) -> Result<()> {
let mut root_store = tls_core::anchors::RootCertStore::empty();
root_store
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let prover = Prover::new(
ProverConfig::builder()
.id("test")
.server_dns(SERVER_DOMAIN)
.max_sent_data(1024)
.max_recv_data(1024)
.root_cert_store(root_store)
.build()
.unwrap(),
)
.setup(io.compat())
.await
.unwrap();
let port: u16 = env::var("SERVER_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_SERVER_PORT);
let addr: IpAddr = env::var("SERVER_IP")
.map(|addr| addr.parse().expect("should be valid IP address"))
.unwrap_or(IpAddr::V4(DEFAULT_SERVER_IP.parse().unwrap()));
let client_socket = TcpStream::connect((addr, port)).await.unwrap();
let (mut tls_connection, prover_fut) = prover.connect(client_socket.compat()).await.unwrap();
let prover_ctrl = prover_fut.control();
let prover_task = tokio::spawn(prover_fut);
// Defer decryption until after the server closes the connection.
prover_ctrl.defer_decryption().await.unwrap();
tls_connection
.write_all(b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
tls_connection.close().await.unwrap();
let mut response = vec![0u8; 1024];
tls_connection.read_to_end(&mut response).await.unwrap();
let mut prover = prover_task.await.unwrap().unwrap().start_prove();
let sent_transcript_len = prover.sent_transcript().data().len();
let recv_transcript_len = prover.recv_transcript().data().len();
// Reveal parts of the transcript
_ = prover.reveal(0..sent_transcript_len - 1, Direction::Sent);
_ = prover.reveal(2..recv_transcript_len, Direction::Received);
prover.prove().await.unwrap();
prover.finalize().await.unwrap();
Ok(())
}

View File

@@ -0,0 +1,49 @@
use std::{env, net::IpAddr};
use anyhow::Result;
use axum::{
http::{HeaderName, HeaderValue},
Router,
};
use futures::Future;
use tokio::net::TcpListener;
use tower::ServiceBuilder;
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer};
use tracing::{info, instrument};
use crate::{DEFAULT_SERVER_IP, DEFAULT_WASM_PORT};
#[instrument]
pub async fn start() -> Result<impl Future<Output = Result<()>>> {
let port: u16 = env::var("WASM_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_WASM_PORT);
let addr: IpAddr = env::var("WASM_IP")
.map(|addr| addr.parse().expect("should be valid IP address"))
.unwrap_or(IpAddr::V4(DEFAULT_SERVER_IP.parse().unwrap()));
let files = ServeDir::new("static");
let service = ServiceBuilder::new()
.layer(SetResponseHeaderLayer::if_not_present(
HeaderName::from_static("cross-origin-embedder-policy"),
HeaderValue::from_static("require-corp"),
))
.layer(SetResponseHeaderLayer::if_not_present(
HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
))
.service(files);
// build our application with a single route
let app = Router::new().fallback_service(service);
let listener = TcpListener::bind((addr, port)).await?;
info!("listening on {}", listener.local_addr()?);
Ok(async move {
axum::serve(listener, app).await?;
Ok(())
})
}

View File

@@ -0,0 +1,26 @@
use std::{env, net::IpAddr};
use anyhow::{Context, Result};
use futures::Future;
use tokio::net::TcpListener;
use tracing::{info, instrument};
use crate::{DEFAULT_SERVER_IP, DEFAULT_WS_PORT};
#[instrument]
pub async fn start() -> Result<impl Future<Output = Result<()>>> {
let port: u16 = env::var("PROXY_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_WS_PORT);
let addr: IpAddr = env::var("PROXY_IP")
.map(|addr| addr.parse().expect("should be valid IP address"))
.unwrap_or(IpAddr::V4(DEFAULT_SERVER_IP.parse().unwrap()));
let listener = TcpListener::bind((addr, port))
.await
.context("failed to bind to address")?;
info!("listening on: {}", listener.local_addr()?);
Ok(websocket_relay::run(listener))
}

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<head>
</head>
<body>
<script src="index.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
const testWorker = Comlink.wrap(new Worker("worker.js", { type: "module" }));
window.testWorker = testWorker;

View File

@@ -0,0 +1,41 @@
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
import init_wasm, { init_logging, initThreadPool } from "./generated/tlsn_wasm.js";
const module = await import("./generated/tlsn_wasm.js");
class TestWorker {
async init() {
try {
await init_wasm();
init_logging();
console.log("initialized logging");
await initThreadPool(8);
console.log("initialized worker");
} catch (e) {
console.error(e);
throw e;
}
}
run() {
let promises = [];
for (const [name, func] of Object.entries(module)) {
if(name.startsWith("test_") && (typeof func === 'function')) {
promises.push(func().then(_ => { return {
name: name,
passed: true,
} }).catch(error => { return {
name: name,
passed: false,
error: error.toString(),
} }));
}
}
return Promise.all(promises);
}
}
const worker = new TestWorker();
Comlink.expose(worker);

View File

@@ -0,0 +1,8 @@
[build]
target = "wasm32-unknown-unknown"
[unstable]
build-std = ["panic_abort", "std"]
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"]

51
crates/wasm/Cargo.toml Normal file
View File

@@ -0,0 +1,51 @@
[package]
name = "tlsn-wasm"
version = "0.1.0-alpha.6"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[package.metadata.wasm-pack.profile.debug]
debug = false
[package.metadata.wasm-pack.profile.release]
opt-level = "z"
wasm-opt = true
[features]
default = []
test = []
[dependencies]
tlsn-core = { path = "../core" }
tlsn-prover = { path = "../prover" }
tlsn-tls-client-async = { path = "../tls/client-async" }
tlsn-tls-core = { path = "../tls/core" }
tlsn-verifier = { path = "../verifier" }
bincode = { workspace = true }
console_error_panic_hook = { version = "0.1" }
enum-try-as-inner = { workspace = true }
futures = { workspace = true }
getrandom = { version = "0.2", features = ["js"] }
http-body-util = { version = "0.1" }
hyper = { workspace = true, features = ["client", "http1"] }
parking_lot = { version = "0.12", features = ["nightly"] }
pin-project-lite = { workspace = true }
ring = { version = "0.17", features = ["wasm32_unknown_unknown_js"] }
serde = { workspace = true, features = ["derive"] }
serde-wasm-bindgen = { version = "0.6" }
serde_json = { version = "1.0" }
time = { version = "0.3", features = ["wasm-bindgen"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["time"] }
tracing-web = { version = "0.1" }
tsify-next = { version = "0.5", default-features = false, features = ["js"] }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
# Use the patched ws_stream_wasm to fix the issue https://github.com/najamelan/ws_stream_wasm/issues/12#issuecomment-1711902958
ws_stream_wasm = { git = "https://github.com/tlsnotary/ws_stream_wasm", rev = "2ed12aad9f0236e5321f577672f309920b2aef51" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-rayon = { version = "1.0", features = ["no-bundler"] }

13
crates/wasm/README.md Normal file
View File

@@ -0,0 +1,13 @@
# TLSNotary WASM bindings
## Build
This crate must be built using the nightly rust compiler with the following flags:
```bash
RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" \
rustup run nightly \
wasm-pack build --target web . -Z build-std=panic_abort,std
```

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

86
crates/wasm/src/io.rs Normal file
View File

@@ -0,0 +1,86 @@
use core::slice;
use std::{
pin::Pin,
task::{Context, Poll},
};
use pin_project_lite::pin_project;
pin_project! {
#[derive(Debug)]
pub(crate) struct FuturesIo<T> {
#[pin]
inner: T,
}
}
impl<T> FuturesIo<T> {
/// Create a new `FuturesIo` wrapping the given I/O object.
///
/// # Safety
///
/// This wrapper is only safe to use if the inner I/O object does not under any circumstance
/// read from the buffer passed to `poll_read` in the `futures::AsyncRead` implementation.
pub(crate) fn new(inner: T) -> Self {
Self { inner }
}
}
impl<T> hyper::rt::Write for FuturesIo<T>
where
T: futures::AsyncWrite + Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
self.project().inner.poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
self.project().inner.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
self.project().inner.poll_close(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>> {
self.project().inner.poll_write_vectored(cx, bufs)
}
}
// Adapted from https://github.com/hyperium/hyper-util/blob/99b77a5a6f75f24bc0bcb4ca74b5f26a07b19c80/src/rt/tokio.rs
impl<T> hyper::rt::Read for FuturesIo<T>
where
T: futures::AsyncRead + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: hyper::rt::ReadBufCursor<'_>,
) -> Poll<Result<(), std::io::Error>> {
// Safety: buf_slice should only be written to, so it's safe to convert `&mut [MaybeUninit<u8>]` to `&mut [u8]`.
let buf_slice = unsafe {
slice::from_raw_parts_mut(buf.as_mut().as_mut_ptr() as *mut u8, buf.as_mut().len())
};
let n = match futures::AsyncRead::poll_read(self.project().inner, cx, buf_slice) {
Poll::Ready(Ok(n)) => n,
other => return other.map_ok(|_| ()),
};
unsafe {
buf.advance(n);
}
Poll::Ready(Ok(()))
}
}

59
crates/wasm/src/lib.rs Normal file
View File

@@ -0,0 +1,59 @@
//! TLSNotary WASM bindings.
#![deny(unreachable_pub, unused_must_use, clippy::all)]
#![allow(non_snake_case)]
pub(crate) mod io;
mod log;
pub mod prover;
#[cfg(feature = "test")]
pub mod tests;
pub mod types;
pub mod verifier;
use log::LoggingConfig;
use tracing::error;
use tracing_subscriber::{
filter::FilterFn,
fmt::{format::FmtSpan, time::UtcTime},
layer::SubscriberExt,
util::SubscriberInitExt,
};
use tracing_web::MakeWebConsoleWriter;
use wasm_bindgen::prelude::*;
#[cfg(feature = "test")]
pub use tests::*;
/// Initializes logging.
#[wasm_bindgen]
pub fn init_logging(config: Option<LoggingConfig>) {
let mut config = config.unwrap_or_default();
// Default is NONE
let fmt_span = config
.span_events
.take()
.unwrap_or_default()
.into_iter()
.map(FmtSpan::from)
.fold(FmtSpan::NONE, |acc, span| acc | span);
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false) // Only partially supported across browsers
.with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers
.with_span_events(fmt_span)
.without_time()
.with_writer(MakeWebConsoleWriter::new()); // write events to the console
tracing_subscriber::registry()
.with(FilterFn::new(log::filter(config)))
.with(fmt_layer)
.init();
// https://github.com/rustwasm/console_error_panic_hook
std::panic::set_hook(Box::new(|info| {
error!("panic occurred: {:?}", info);
console_error_panic_hook::hook(info);
}));
}

88
crates/wasm/src/log.rs Normal file
View File

@@ -0,0 +1,88 @@
use serde::Deserialize;
use tracing::{Level, Metadata};
use tracing_subscriber::fmt::format::FmtSpan;
use tsify_next::Tsify;
#[derive(Debug, Clone, Copy, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub enum LoggingLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl From<LoggingLevel> for Level {
fn from(value: LoggingLevel) -> Self {
match value {
LoggingLevel::Trace => Level::TRACE,
LoggingLevel::Debug => Level::DEBUG,
LoggingLevel::Info => Level::INFO,
LoggingLevel::Warn => Level::WARN,
LoggingLevel::Error => Level::ERROR,
}
}
}
#[derive(Debug, Clone, Copy, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub enum SpanEvent {
New,
Close,
Active,
}
impl From<SpanEvent> for FmtSpan {
fn from(value: SpanEvent) -> Self {
match value {
SpanEvent::New => FmtSpan::NEW,
SpanEvent::Close => FmtSpan::CLOSE,
SpanEvent::Active => FmtSpan::ACTIVE,
}
}
}
#[derive(Debug, Default, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct LoggingConfig {
pub level: Option<LoggingLevel>,
pub crate_filters: Option<Vec<CrateLogFilter>>,
pub span_events: Option<Vec<SpanEvent>>,
}
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct CrateLogFilter {
pub level: LoggingLevel,
pub name: String,
}
pub(crate) fn filter(config: LoggingConfig) -> impl Fn(&Metadata) -> bool {
let default_level: Level = config.level.unwrap_or(LoggingLevel::Info).into();
let crate_filters = config
.crate_filters
.unwrap_or_default()
.into_iter()
.map(|filter| (filter.name, Level::from(filter.level)))
.collect::<Vec<_>>();
move |meta| {
let level = if let Some(crate_name) = meta.target().split("::").next() {
crate_filters
.iter()
.find_map(|(filter_name, filter_level)| {
if crate_name.eq_ignore_ascii_case(filter_name) {
Some(filter_level)
} else {
None
}
})
.unwrap_or(&default_level)
} else {
&default_level
};
meta.level() <= level
}
}

View File

@@ -0,0 +1,29 @@
use serde::Deserialize;
use tsify_next::Tsify;
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct ProverConfig {
pub id: String,
pub server_dns: String,
pub max_sent_data: Option<usize>,
pub max_recv_data: Option<usize>,
}
impl From<ProverConfig> for tlsn_prover::tls::ProverConfig {
fn from(value: ProverConfig) -> Self {
let mut builder = tlsn_prover::tls::ProverConfig::builder();
builder.id(value.id);
builder.server_dns(value.server_dns);
if let Some(value) = value.max_sent_data {
builder.max_sent_data(value);
}
if let Some(value) = value.max_recv_data {
builder.max_recv_data(value);
}
builder.build().unwrap()
}
}

View File

@@ -0,0 +1,204 @@
mod config;
pub use config::ProverConfig;
use enum_try_as_inner::EnumTryAsInner;
use futures::TryFutureExt;
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use tls_client_async::TlsConnection;
use tlsn_core::Direction;
use tlsn_prover::tls::{state, Prover};
use tracing::info;
use wasm_bindgen::{prelude::*, JsError};
use wasm_bindgen_futures::spawn_local;
use ws_stream_wasm::WsMeta;
use crate::{io::FuturesIo, types::*};
type Result<T> = std::result::Result<T, JsError>;
#[wasm_bindgen(js_name = Prover)]
pub struct JsProver {
state: State,
}
#[derive(Debug, EnumTryAsInner)]
#[derive_err(Debug)]
enum State {
Initialized(Prover<state::Initialized>),
Setup(Prover<state::Setup>),
Closed(Prover<state::Closed>),
Complete,
Error,
}
impl State {
fn take(&mut self) -> Self {
std::mem::replace(self, State::Error)
}
}
#[wasm_bindgen(js_class = Prover)]
impl JsProver {
#[wasm_bindgen(constructor)]
pub fn new(config: ProverConfig) -> JsProver {
JsProver {
state: State::Initialized(Prover::new(config.into())),
}
}
/// Set up the prover.
///
/// This performs all MPC setup prior to establishing the connection to the
/// application server.
pub async fn setup(&mut self, verifier_url: &str) -> Result<()> {
let prover = self.state.take().try_into_initialized()?;
info!("connecting to verifier");
let (_, verifier_conn) = WsMeta::connect(verifier_url, None).await?;
info!("connected to verifier");
let prover = prover.setup(verifier_conn.into_io()).await?;
self.state = State::Setup(prover);
Ok(())
}
/// Send the HTTP request to the server.
pub async fn send_request(
&mut self,
ws_proxy_url: &str,
request: HttpRequest,
) -> Result<HttpResponse> {
let prover = self.state.take().try_into_setup()?;
info!("connecting to server");
let (_, server_conn) = WsMeta::connect(ws_proxy_url, None).await?;
info!("connected to server");
let (tls_conn, prover_fut) = prover.connect(server_conn.into_io()).await?;
let prover_ctrl = prover_fut.control();
info!("sending request");
let (response, prover) = futures::try_join!(
async move {
prover_ctrl.defer_decryption().await?;
send_request(tls_conn, request).await
},
prover_fut.map_err(Into::into),
)?;
info!("response received");
self.state = State::Closed(prover);
Ok(response)
}
/// Returns the transcript.
pub fn transcript(&self) -> Result<Transcript> {
let prover = self.state.try_as_closed()?;
let sent = prover.sent_transcript().data().clone();
let recv = prover.recv_transcript().data().clone();
Ok(Transcript {
sent: sent.to_vec(),
recv: recv.to_vec(),
})
}
/// Runs the notarization protocol.
pub async fn notarize(&mut self, commit: Commit) -> Result<NotarizedSession> {
let mut prover = self.state.take().try_into_closed()?.start_notarize();
info!("starting notarization");
let builder = prover.commitment_builder();
for range in commit.sent {
builder.commit_sent(&range)?;
}
for range in commit.recv {
builder.commit_recv(&range)?;
}
let notarized_session = prover.finalize().await?;
info!("notarization complete");
Ok(notarized_session.into())
}
/// Reveals data to the verifier and finalizes the protocol.
pub async fn reveal(&mut self, reveal: Reveal) -> Result<()> {
let mut prover = self.state.take().try_into_closed()?.start_prove();
info!("revealing data");
for range in reveal.sent {
prover.reveal(range, Direction::Sent)?;
}
for range in reveal.recv {
prover.reveal(range, Direction::Received)?;
}
prover.prove().await?;
prover.finalize().await?;
info!("Finalized");
self.state = State::Complete;
Ok(())
}
}
impl From<Prover<state::Initialized>> for JsProver {
fn from(value: Prover<state::Initialized>) -> Self {
JsProver {
state: State::Initialized(value),
}
}
}
async fn send_request(conn: TlsConnection, request: HttpRequest) -> Result<HttpResponse> {
let conn = FuturesIo::new(conn);
let request = hyper::Request::<Full<Bytes>>::try_from(request)?;
let (mut request_sender, conn) = hyper::client::conn::http1::handshake(conn).await?;
spawn_local(async move { conn.await.expect("connection runs to completion") });
let response = request_sender.send_request(request).await?;
let (response, body) = response.into_parts();
// TODO: return the body
let _body = body.collect().await?;
let headers = response
.headers
.into_iter()
.map(|(k, v)| {
(
k.map(|k| k.to_string()).unwrap_or_default(),
v.as_bytes().to_vec(),
)
})
.collect();
Ok(HttpResponse {
status: response.status.as_u16(),
headers,
})
}

142
crates/wasm/src/tests.rs Normal file
View File

@@ -0,0 +1,142 @@
#![allow(clippy::single_range_in_vec_init)]
use std::collections::HashMap;
use tls_core::verify::WebPkiVerifier;
use tlsn_prover::tls::{Prover, ProverConfig};
use tlsn_verifier::tls::{Verifier, VerifierConfig};
use wasm_bindgen::prelude::*;
use crate::{
prover::JsProver,
types::{Commit, HttpRequest, Method, Reveal},
verifier::JsVerifier,
};
#[cfg(target_arch = "wasm32")]
pub use wasm_bindgen_rayon::init_thread_pool;
static CA_CERT_DER: &[u8] = include_bytes!("../../server-fixture/src/tls/root_ca_cert.der");
static SERVER_DOMAIN: &str = "test-server.io";
#[wasm_bindgen]
pub async fn test_prove() -> Result<(), JsValue> {
let mut root_store = tls_core::anchors::RootCertStore::empty();
root_store
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let prover = Prover::new(
ProverConfig::builder()
.id("test")
.server_dns(SERVER_DOMAIN)
.root_cert_store(root_store)
.max_sent_data(1024)
.max_recv_data(1024)
.build()
.unwrap(),
);
let mut prover = JsProver::from(prover);
let uri = format!("https://{}/bytes?size=512", SERVER_DOMAIN);
prover
.setup("ws://localhost:8080/tcp?addr=localhost%3A8010")
.await?;
prover
.send_request(
"ws://localhost:8080/tcp?addr=localhost%3A8083",
HttpRequest {
method: Method::GET,
uri,
headers: HashMap::from([("Accept".to_string(), b"*".to_vec())]),
body: None,
},
)
.await?;
prover
.reveal(Reveal {
sent: vec![0..10],
recv: vec![0..10],
})
.await?;
Ok(())
}
#[wasm_bindgen]
pub async fn test_notarize() -> Result<(), JsValue> {
let mut root_store = tls_core::anchors::RootCertStore::empty();
root_store
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let prover = Prover::new(
ProverConfig::builder()
.id("test")
.server_dns(SERVER_DOMAIN)
.root_cert_store(root_store)
.max_sent_data(1024)
.max_recv_data(1024)
.build()
.unwrap(),
);
let mut prover = JsProver::from(prover);
let uri = format!("https://{SERVER_DOMAIN}/bytes?size=512");
prover
.setup("ws://localhost:8080/tcp?addr=localhost%3A8011")
.await?;
prover
.send_request(
"ws://localhost:8080/tcp?addr=localhost%3A8083",
HttpRequest {
method: Method::GET,
uri,
headers: HashMap::from([("Accept".to_string(), b"*".to_vec())]),
body: None,
},
)
.await?;
let _ = prover.transcript()?;
prover
.notarize(Commit {
sent: vec![0..10],
recv: vec![0..10],
})
.await?;
Ok(())
}
#[wasm_bindgen]
pub async fn test_verifier() -> Result<(), JsValue> {
let mut root_store = tls_core::anchors::RootCertStore::empty();
root_store
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let config = VerifierConfig::builder()
.id("test")
.max_sent_data(1024)
.max_recv_data(1024)
.cert_verifier(WebPkiVerifier::new(root_store, None))
.build()
.unwrap();
let mut verifier = JsVerifier::from(Verifier::new(config));
verifier
.connect("ws://localhost:8080/tcp?addr=localhost%3A8012")
.await?;
verifier.verify().await?;
Ok(())
}

173
crates/wasm/src/types.rs Normal file
View File

@@ -0,0 +1,173 @@
use std::{collections::HashMap, ops::Range};
use http_body_util::Full;
use hyper::body::Bytes;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use tlsn_core::commitment::CommitmentKind;
use tsify_next::Tsify;
use wasm_bindgen::prelude::*;
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Body {
Json(JsonValue),
}
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub enum Method {
GET,
POST,
PUT,
DELETE,
}
impl From<Method> for hyper::Method {
fn from(value: Method) -> Self {
match value {
Method::GET => hyper::Method::GET,
Method::POST => hyper::Method::POST,
Method::PUT => hyper::Method::PUT,
Method::DELETE => hyper::Method::DELETE,
}
}
}
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct HttpRequest {
pub uri: String,
pub method: Method,
pub headers: HashMap<String, Vec<u8>>,
pub body: Option<Body>,
}
impl TryFrom<HttpRequest> for hyper::Request<Full<Bytes>> {
type Error = JsError;
fn try_from(value: HttpRequest) -> Result<Self, Self::Error> {
let mut builder = hyper::Request::builder();
builder = builder.uri(value.uri).method(value.method);
for (name, value) in value.headers {
builder = builder.header(name, value);
}
if let Some(body) = value.body {
let body = match body {
Body::Json(value) => Full::new(Bytes::from(serde_json::to_vec(&value).unwrap())),
};
builder.body(body).map_err(Into::into)
} else {
builder.body(Full::new(Bytes::new())).map_err(Into::into)
}
}
}
#[derive(Debug, Tsify, Serialize)]
#[tsify(into_wasm_abi)]
pub struct HttpResponse {
pub status: u16,
pub headers: Vec<(String, Vec<u8>)>,
}
#[derive(Debug, Tsify, Serialize)]
#[tsify(into_wasm_abi)]
pub struct Transcript {
pub sent: Vec<u8>,
pub recv: Vec<u8>,
}
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct Commit {
pub sent: Vec<Range<usize>>,
pub recv: Vec<Range<usize>>,
}
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct Reveal {
pub sent: Vec<Range<usize>>,
pub recv: Vec<Range<usize>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[wasm_bindgen]
#[serde(transparent)]
pub struct NotarizedSession(tlsn_core::NotarizedSession);
#[wasm_bindgen]
impl NotarizedSession {
/// Builds a new proof.
pub fn proof(&self, reveal: Reveal) -> Result<TlsProof, JsError> {
let mut builder = self.0.data().build_substrings_proof();
for range in reveal.sent.iter() {
builder.reveal_sent(range, CommitmentKind::Blake3)?;
}
for range in reveal.recv.iter() {
builder.reveal_recv(range, CommitmentKind::Blake3)?;
}
let substring_proof = builder.build()?;
Ok(TlsProof(tlsn_core::proof::TlsProof {
session: self.0.session_proof(),
substrings: substring_proof,
}))
}
/// Returns the transcript.
pub fn transcript(&self) -> Transcript {
Transcript {
sent: self.0.data().sent_transcript().data().to_vec(),
recv: self.0.data().recv_transcript().data().to_vec(),
}
}
/// Serializes to a byte array.
pub fn serialize(&self) -> Vec<u8> {
bincode::serialize(self).expect("NotarizedSession is serializable")
}
/// Deserializes from a byte array.
pub fn deserialize(bytes: Vec<u8>) -> Result<NotarizedSession, JsError> {
Ok(bincode::deserialize(&bytes)?)
}
}
impl From<tlsn_core::NotarizedSession> for NotarizedSession {
fn from(value: tlsn_core::NotarizedSession) -> Self {
Self(value)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[wasm_bindgen]
#[serde(transparent)]
pub struct TlsProof(tlsn_core::proof::TlsProof);
#[wasm_bindgen]
impl TlsProof {
pub fn serialize(&self) -> Vec<u8> {
bincode::serialize(self).expect("TlsProof is serializable")
}
pub fn deserialize(bytes: Vec<u8>) -> Result<TlsProof, JsError> {
Ok(bincode::deserialize(&bytes)?)
}
}
#[derive(Debug, Tsify, Serialize)]
#[tsify(into_wasm_abi)]
pub struct VerifierData {
pub server_dns: String,
pub sent: Vec<u8>,
pub sent_auth_ranges: Vec<Range<usize>>,
pub received: Vec<u8>,
pub received_auth_ranges: Vec<Range<usize>>,
}

View File

@@ -0,0 +1,26 @@
use serde::Deserialize;
use tsify_next::Tsify;
#[derive(Debug, Tsify, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct VerifierConfig {
pub id: String,
pub max_sent_data: Option<usize>,
pub max_received_data: Option<usize>,
}
impl From<VerifierConfig> for tlsn_verifier::tls::VerifierConfig {
fn from(value: VerifierConfig) -> Self {
let mut builder = tlsn_verifier::tls::VerifierConfig::builder();
if let Some(value) = value.max_sent_data {
builder = builder.max_sent_data(value);
}
if let Some(value) = value.max_received_data {
builder = builder.max_recv_data(value);
}
builder.id(value.id).build().unwrap()
}
}

View File

@@ -0,0 +1,92 @@
mod config;
pub use config::VerifierConfig;
use enum_try_as_inner::EnumTryAsInner;
use tlsn_verifier::tls::{
state::{self, Initialized},
Verifier,
};
use tracing::info;
use wasm_bindgen::prelude::*;
use ws_stream_wasm::{WsMeta, WsStream};
use crate::types::VerifierData;
type Result<T> = std::result::Result<T, JsError>;
#[wasm_bindgen(js_name = Verifier)]
pub struct JsVerifier {
state: State,
}
#[derive(EnumTryAsInner)]
#[derive_err(Debug)]
enum State {
Initialized(Verifier<state::Initialized>),
Connected((Verifier<state::Initialized>, WsStream)),
Complete,
Error,
}
impl std::fmt::Debug for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "State")
}
}
impl State {
fn take(&mut self) -> Self {
std::mem::replace(self, State::Error)
}
}
#[wasm_bindgen(js_class = Verifier)]
impl JsVerifier {
#[wasm_bindgen(constructor)]
pub fn new(config: VerifierConfig) -> JsVerifier {
JsVerifier {
state: State::Initialized(Verifier::new(config.into())),
}
}
/// Connect to the prover.
pub async fn connect(&mut self, prover_url: &str) -> Result<()> {
let verifier = self.state.take().try_into_initialized()?;
info!("Connecting to prover");
let (_, prover_conn) = WsMeta::connect(prover_url, None).await?;
info!("Connected to prover");
self.state = State::Connected((verifier, prover_conn));
Ok(())
}
/// Verifies the connection and finalizes the protocol.
pub async fn verify(&mut self) -> Result<VerifierData> {
let (verifier, prover_conn) = self.state.take().try_into_connected()?;
let (sent, recv, info) = verifier.verify(prover_conn.into_io()).await?;
self.state = State::Complete;
Ok(VerifierData {
server_dns: info.server_name.as_str().to_string(),
sent: sent.data().to_vec(),
sent_auth_ranges: sent.authed().iter_ranges().collect(),
received: recv.data().to_vec(),
received_auth_ranges: recv.authed().iter_ranges().collect(),
})
}
}
impl From<tlsn_verifier::tls::Verifier<Initialized>> for JsVerifier {
fn from(value: tlsn_verifier::tls::Verifier<Initialized>) -> Self {
Self {
state: State::Initialized(value),
}
}
}