From a94b82462c8beb6c1b300eac9f396121c1a4dd9a Mon Sep 17 00:00:00 2001 From: Hendrik Eeckhaut Date: Mon, 11 Mar 2024 15:08:22 +0100 Subject: [PATCH] refactor: Split Prover and Verifier in separate files # Conflicts: # wasm/prover/src/lib.rs --- wasm/prover/src/lib.rs | 587 +------------------------------------- wasm/prover/src/prover.rs | 482 +++++++++++++++++++++++++++++++ wasm/prover/src/verify.rs | 107 +++++++ 3 files changed, 598 insertions(+), 578 deletions(-) create mode 100644 wasm/prover/src/prover.rs create mode 100644 wasm/prover/src/verify.rs diff --git a/wasm/prover/src/lib.rs b/wasm/prover/src/lib.rs index b033a8c..c1c160f 100644 --- a/wasm/prover/src/lib.rs +++ b/wasm/prover/src/lib.rs @@ -1,37 +1,22 @@ mod request_opt; mod requests; -use std::ops::Range; -use std::panic; -use web_time::Instant; +pub mod prover; +pub use prover::prover; -use futures::channel::oneshot; -use futures::AsyncWriteExt; -use hyper::{body::to_bytes, Body, Request, StatusCode}; -use tlsn_prover::tls::{Prover, ProverConfig}; - -use tokio_util::compat::FuturesAsyncReadCompatExt; +pub mod verify; +pub use verify::verify; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::spawn_local; - -use ws_stream_wasm::*; pub use crate::request_opt::{RequestOptions, VerifyResult}; -use crate::requests::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse}; pub use wasm_bindgen_rayon::init_thread_pool; -use js_sys::{Array, JSON}; -use url::Url; +use js_sys::JSON; + use wasm_bindgen_futures::JsFuture; -use web_sys::{Headers, Request as WebsysRequest, RequestInit, RequestMode, Response}; - -use elliptic_curve::pkcs8::DecodePublicKey; -use std::time::Duration; -use tlsn_core::proof::{SessionProof, TlsProof}; - -use strum::EnumMessage; +use web_sys::{Request as WebsysRequest, RequestInit, Response}; // A macro to provide `println!(..)`-style syntax for `console.log` logging. macro_rules! log { @@ -39,6 +24,7 @@ macro_rules! log { web_sys::console::log_1(&format!( $( $t )* ).into()); } } +pub(crate) use log; extern crate console_error_panic_hook; @@ -48,7 +34,7 @@ extern "C" { fn fetch(request: &web_sys::Request) -> js_sys::Promise; } -async fn fetch_as_json_string(url: &str, opts: &RequestInit) -> Result { +pub async fn fetch_as_json_string(url: &str, opts: &RequestInit) -> Result { let request = WebsysRequest::new_with_str_and_init(url, opts)?; let promise = fetch(&request); let future = JsFuture::from(promise); @@ -60,558 +46,3 @@ async fn fetch_as_json_string(url: &str, opts: &RequestInit) -> Result Result { - log!("target_url: {}", target_url_str); - let target_url = Url::parse(target_url_str) - .map_err(|e| JsValue::from_str(&format!("Could not parse target_url: {:?}", e)))?; - - log!( - "target_url.host: {}", - target_url - .host() - .ok_or(JsValue::from_str("Could not get target host"))? - ); - let options: RequestOptions = serde_wasm_bindgen::from_value(val) - .map_err(|e| JsValue::from_str(&format!("Could not deserialize options: {:?}", e)))?; - log!("options.notary_url: {}", options.notary_url.as_str()); - - // 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_writer(MakeConsoleWriter); // write events to the console - // let perf_layer = performance_layer() - // .with_details_from_fields(Pretty::default()); - - // tracing_subscriber::registry() - // .with(tracing_subscriber::filter::LevelFilter::DEBUG) - // .with(fmt_layer) - // .with(perf_layer) - // .init(); // Install these as subscribers to tracing events - - // https://github.com/rustwasm/console_error_panic_hook - panic::set_hook(Box::new(console_error_panic_hook::hook)); - - let start_time = Instant::now(); - - /* - * Connect Notary with websocket - */ - - let mut opts = RequestInit::new(); - log!("method: {}", "POST"); - opts.method("POST"); - // opts.method("GET"); - opts.mode(RequestMode::Cors); - - // set headers - let headers = Headers::new() - .map_err(|e| JsValue::from_str(&format!("Could not create headers: {:?}", e)))?; - let notary_url = Url::parse(options.notary_url.as_str()) - .map_err(|e| JsValue::from_str(&format!("Could not parse notary_url: {:?}", e)))?; - let notary_ssl = notary_url.scheme() == "https" || notary_url.scheme() == "wss"; - let notary_host = notary_url.authority(); - - headers - .append("Host", notary_host) - .map_err(|e| JsValue::from_str(&format!("Could not append Host header: {:?}", e)))?; - headers - .append("Content-Type", "application/json") - .map_err(|e| { - JsValue::from_str(&format!("Could not append Content-Type header: {:?}", e)) - })?; - opts.headers(&headers); - - log!("notary_host: {}", notary_host); - // set body - let payload = serde_json::to_string(&NotarizationSessionRequest { - client_type: ClientType::Websocket, - max_transcript_size: Some(options.max_transcript_size), - }) - .map_err(|e| JsValue::from_str(&format!("Could not serialize request: {:?}", e)))?; - opts.body(Some(&JsValue::from_str(&payload))); - - // url - let url = format!( - "{}://{}/session", - if notary_ssl { "https" } else { "http" }, - notary_host - ); - log!("Request: {}", url); - let rust_string = fetch_as_json_string(&url, &opts) - .await - .map_err(|e| JsValue::from_str(&format!("Could not fetch session: {:?}", e)))?; - let notarization_response = - serde_json::from_str::(&rust_string) - .map_err(|e| JsValue::from_str(&format!("Could not deserialize response: {:?}", e)))?; - log!("Response: {}", rust_string); - - log!("Notarization response: {:?}", notarization_response,); - let notary_wss_url = format!( - "{}://{}/notarize?sessionId={}", - if notary_ssl { "wss" } else { "ws" }, - notary_host, - notarization_response.session_id - ); - let (_, notary_ws_stream) = WsMeta::connect(notary_wss_url, None) - .await - .expect_throw("assume the notary ws connection succeeds"); - let notary_ws_stream_into = notary_ws_stream.into_io(); - - log_phase(ProverPhases::BuildProverConfig); - - let target_host = target_url - .host_str() - .ok_or(JsValue::from_str("Could not get target host"))?; - // Basic default prover config - let config = ProverConfig::builder() - .id(notarization_response.session_id) - .server_dns(target_host) - .max_transcript_size(options.max_transcript_size) - .build() - .map_err(|e| JsValue::from_str(&format!("Could not build prover config: {:?}", e)))?; - - // Create a Prover and set it up with the Notary - // This will set up the MPC backend prior to connecting to the server. - log_phase(ProverPhases::SetUpProver); - let prover = Prover::new(config) - .setup(notary_ws_stream_into) - .await - .map_err(|e| JsValue::from_str(&format!("Could not set up prover: {:?}", e)))?; - - /* - Connect Application Server with websocket proxy - */ - log_phase(ProverPhases::ConnectWsProxy); - - let (_, client_ws_stream) = WsMeta::connect(options.websocket_proxy_url, None) - .await - .expect_throw("assume the client ws connection succeeds"); - let client_ws_stream_into = client_ws_stream.into_io(); - - // Bind the Prover to the server connection. - // The returned `mpc_tls_connection` is an MPC TLS connection to the Server: all data written - // to/read from it will be encrypted/decrypted using MPC with the Notary. - log_phase(ProverPhases::BindProverToConnection); - let (mpc_tls_connection, prover_fut) = prover - .connect(client_ws_stream_into) - .await - .map_err(|e| JsValue::from_str(&format!("Could not connect prover: {:?}", e)))?; - - let prover_ctrl = prover_fut.control(); - - log_phase(ProverPhases::SpawnProverThread); - let (prover_sender, prover_receiver) = oneshot::channel(); - let handled_prover_fut = async { - let result = prover_fut.await; - let _ = prover_sender.send(result); - }; - spawn_local(handled_prover_fut); - - // Attach the hyper HTTP client to the TLS connection - log_phase(ProverPhases::AttachHttpClient); - let (mut request_sender, connection) = - hyper::client::conn::handshake(mpc_tls_connection.compat()) - .await - .map_err(|e| JsValue::from_str(&format!("Could not handshake: {:?}", e)))?; - - // Spawn the HTTP task to be run concurrently - log_phase(ProverPhases::SpawnHttpTask); - let (connection_sender, connection_receiver) = oneshot::channel(); - let connection_fut = connection.without_shutdown(); - let handled_connection_fut = async { - let result = connection_fut.await; - let _ = connection_sender.send(result); - }; - spawn_local(handled_connection_fut); - - log_phase(ProverPhases::BuildRequest); - let mut req_with_header = Request::builder() - .uri(target_url_str) - .method(options.method.as_str()); - - for (key, value) in options.headers { - log!("adding header: {} - {}", key.as_str(), value.as_str()); - req_with_header = req_with_header.header(key.as_str(), value.as_str()); - } - - let req_with_body = if options.body.is_empty() { - log!("empty body"); - req_with_header.body(Body::empty()) - } else { - log!("added body - {}", options.body.as_str()); - req_with_header.body(Body::from(options.body)) - }; - - let unwrapped_request = req_with_body - .map_err(|e| JsValue::from_str(&format!("Could not build request: {:?}", e)))?; - - log_phase(ProverPhases::StartMpcConnection); - - // Defer decryption of the response. - prover_ctrl - .defer_decryption() - .await - .map_err(|e| JsValue::from_str(&format!("failed to enable deferred decryption: {}", e)))?; - - // Send the request to the Server and get a response via the MPC TLS connection - let response = request_sender - .send_request(unwrapped_request) - .await - .map_err(|e| JsValue::from_str(&format!("Could not send request: {:?}", e)))?; - - log_phase(ProverPhases::ReceivedResponse); - if response.status() != StatusCode::OK { - return Err(JsValue::from_str(&format!( - "Response status is not OK: {:?}", - response.status() - ))); - } - - log_phase(ProverPhases::ParseResponse); - // Pretty printing :) - let payload = to_bytes(response.into_body()) - .await - .map_err(|e| JsValue::from_str(&format!("Could not get response body: {:?}", e)))? - .to_vec(); - let parsed = serde_json::from_str::(&String::from_utf8_lossy(&payload)) - .map_err(|e| JsValue::from_str(&format!("Could not parse response: {:?}", e)))?; - let response_pretty = serde_json::to_string_pretty(&parsed) - .map_err(|e| JsValue::from_str(&format!("Could not serialize response: {:?}", e)))?; - log!("Response: {}", response_pretty); - - // Close the connection to the server - log_phase(ProverPhases::CloseConnection); - let mut client_socket = connection_receiver - .await - .map_err(|e| { - JsValue::from_str(&format!( - "Could not receive from connection_receiver: {:?}", - e - )) - })? - .map_err(|e| JsValue::from_str(&format!("Could not get TlsConnection: {:?}", e)))? - .io - .into_inner(); - client_socket - .close() - .await - .map_err(|e| JsValue::from_str(&format!("Could not close socket: {:?}", e)))?; - - // The Prover task should be done now, so we can grab it. - log_phase(ProverPhases::StartNotarization); - let prover = prover_receiver - .await - .map_err(|e| { - JsValue::from_str(&format!("Could not receive from prover_receiver: {:?}", e)) - })? - .map_err(|e| JsValue::from_str(&format!("Could not get Prover: {:?}", e)))?; - let mut prover = prover.start_notarize(); - - let secret_headers_vecs = string_list_to_bytes_vec(&secret_headers)?; - let secret_headers_slices: Vec<&[u8]> = secret_headers_vecs - .iter() - .map(|vec| vec.as_slice()) - .collect(); - - // Identify the ranges in the transcript that contain revealed_headers - let (sent_public_ranges, sent_private_ranges) = find_ranges( - prover.sent_transcript().data(), - secret_headers_slices.as_slice(), - ); - - let secret_body_vecs = string_list_to_bytes_vec(&secret_body)?; - let secret_body_slices: Vec<&[u8]> = - secret_body_vecs.iter().map(|vec| vec.as_slice()).collect(); - - // Identify the ranges in the transcript that contain the only data we want to reveal later - let (recv_public_ranges, recv_private_ranges) = find_ranges( - prover.recv_transcript().data(), - secret_body_slices.as_slice(), - ); - - log_phase(ProverPhases::Commit); - - let _recv_len = prover.recv_transcript().data().len(); - - let builder = prover.commitment_builder(); - - // Commit to the outbound and inbound transcript, isolating the data that contain secrets - let sent_pub_commitment_ids = sent_public_ranges - .iter() - .map(|range| { - builder.commit_sent(range).map_err(|e| { - JsValue::from_str(&format!("Error committing sent pub range: {:?}", e)) - }) - }) - .collect::, _>>()?; - - sent_private_ranges.iter().try_for_each(|range| { - builder - .commit_sent(range) - .map_err(|e| { - JsValue::from_str(&format!("Error committing sent private range: {:?}", e)) - }) - .map(|_| ()) - })?; - - let recv_pub_commitment_ids = recv_public_ranges - .iter() - .map(|range| { - builder.commit_recv(range).map_err(|e| { - JsValue::from_str(&format!("Error committing recv public ranges: {:?}", e)) - }) - }) - .collect::, _>>()?; - - recv_private_ranges.iter().try_for_each(|range| { - builder - .commit_recv(range) - .map_err(|e| { - JsValue::from_str(&format!("Error committing recv private range: {:?}", e)) - }) - .map(|_| ()) - })?; - - // Finalize, returning the notarized session - log_phase(ProverPhases::Finalize); - let notarized_session = prover - .finalize() - .await - .map_err(|e| JsValue::from_str(&format!("Error finalizing prover: {:?}", e)))?; - - log_phase(ProverPhases::NotarizationComplete); - - // Create a proof for all committed data in this session - log_phase(ProverPhases::CreateProof); - let session_proof = notarized_session.session_proof(); - - let mut proof_builder = notarized_session.data().build_substrings_proof(); - - // Reveal everything except the redacted stuff (which for the response it's everything except the screen_name) - sent_pub_commitment_ids - .iter() - .chain(recv_pub_commitment_ids.iter()) - .try_for_each(|id| { - proof_builder - .reveal_by_id(*id) - .map_err(|e| JsValue::from_str(&format!("Could not reveal commitment: {:?}", e))) - .map(|_| ()) - })?; - - let substrings_proof = proof_builder - .build() - .map_err(|e| JsValue::from_str(&format!("Could not build proof: {:?}", e)))?; - - let proof = TlsProof { - session: session_proof, - substrings: substrings_proof, - }; - - let res = serde_json::to_string_pretty(&proof) - .map_err(|e| JsValue::from_str(&format!("Could not serialize proof: {:?}", e)))?; - - let duration = start_time.elapsed(); - log!("!@# request took {} seconds", duration.as_secs()); - - Ok(res) -} - -#[wasm_bindgen] -pub async fn verify(proof: &str, notary_pubkey_str: &str) -> Result { - // log!("!@# proof {}", proof); - let proof: TlsProof = serde_json::from_str(proof) - .map_err(|e| JsValue::from_str(&format!("Could not deserialize proof: {:?}", e)))?; - - 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; - - log!( - "!@# notary_pubkey {}, {}", - notary_pubkey_str, - notary_pubkey_str.len() - ); - session - .verify_with_default_cert_verifier(get_notary_pubkey(notary_pubkey_str)?) - .map_err(|e| JsValue::from_str(&format!("Session verification failed: {:?}", e)))?; - - let SessionProof { - // The session header that was signed by the Notary is a succinct commitment to the TLS transcript. - header, - // This is the server name, 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) - .map_err(|e| JsValue::from_str(&format!("Could not verify substrings: {:?}", e)))?; - - // Replace the bytes which the Prover chose not to disclose with 'X' - sent.set_redacted(b'X'); - recv.set_redacted(b'X'); - - log!("-------------------------------------------------------------------"); - log!( - "Successfully verified that the bytes below came from a session with {:?} at {}.", - session_info.server_name, - time - ); - log!("Note that the bytes which the Prover chose not to disclose are shown as X."); - log!("Bytes sent:"); - log!( - "{}", - String::from_utf8(sent.data().to_vec()).map_err(|e| JsValue::from_str(&format!( - "Could not convert sent data to string: {:?}", - e - )))? - ); - log!("Bytes received:"); - log!( - "{}", - String::from_utf8(recv.data().to_vec()).map_err(|e| JsValue::from_str(&format!( - "Could not convert recv data to string: {:?}", - e - )))? - ); - log!("-------------------------------------------------------------------"); - - let result = VerifyResult { - server_name: String::from(session_info.server_name.as_str()), - time: header.time(), - sent: String::from_utf8(sent.data().to_vec()).map_err(|e| { - JsValue::from_str(&format!("Could not convert sent data to string: {:?}", e)) - })?, - recv: String::from_utf8(recv.data().to_vec()).map_err(|e| { - JsValue::from_str(&format!("Could not convert recv data to string: {:?}", e)) - })?, - }; - let res = serde_json::to_string_pretty(&result) - .map_err(|e| JsValue::from_str(&format!("Could not serialize result: {:?}", e)))?; - - Ok(res) -} - -#[allow(unused)] -fn print_type_of(_: &T) { - log!("{}", std::any::type_name::()); -} - -/// Returns a Notary pubkey trusted by this Verifier -fn get_notary_pubkey(pubkey: &str) -> Result { - p256::PublicKey::from_public_key_pem(pubkey) - .map_err(|e| JsValue::from_str(&format!("Could not get notary pubkey: {:?}", e))) -} - -/// Find the ranges of the public and private parts of a sequence. -/// -/// Returns a tuple of `(public, private)` ranges. -fn find_ranges(seq: &[u8], private_seq: &[&[u8]]) -> (Vec>, Vec>) { - let mut private_ranges = Vec::new(); - for s in private_seq { - for (idx, w) in seq.windows(s.len()).enumerate() { - if w == *s { - private_ranges.push(idx..(idx + w.len())); - } - } - } - - let mut sorted_ranges = private_ranges.clone(); - sorted_ranges.sort_by_key(|r| r.start); - - let mut public_ranges = Vec::new(); - let mut last_end = 0; - for r in sorted_ranges { - if r.start > last_end { - public_ranges.push(last_end..r.start); - } - last_end = r.end; - } - - if last_end < seq.len() { - public_ranges.push(last_end..seq.len()); - } - - (public_ranges, private_ranges) -} - -fn string_list_to_bytes_vec(secrets: &JsValue) -> Result>, JsValue> { - let array: Array = Array::from(secrets); - let length = array.length(); - let mut byte_slices: Vec> = Vec::new(); - - for i in 0..length { - let secret_js: JsValue = array.get(i); - let secret_str: String = secret_js - .as_string() - .ok_or(JsValue::from_str("Could not convert secret to string"))?; - let secret_bytes = secret_str.into_bytes(); - byte_slices.push(secret_bytes); - } - Ok(byte_slices) -} diff --git a/wasm/prover/src/prover.rs b/wasm/prover/src/prover.rs new file mode 100644 index 0000000..9a33bd8 --- /dev/null +++ b/wasm/prover/src/prover.rs @@ -0,0 +1,482 @@ +use futures::channel::oneshot; +use std::ops::Range; +use std::panic; +use tlsn_prover::tls::{Prover, ProverConfig}; +use wasm_bindgen_futures::spawn_local; +use web_time::Instant; + +use ws_stream_wasm::*; + +use crate::request_opt::RequestOptions; +use crate::requests::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse}; + +pub use wasm_bindgen_rayon::init_thread_pool; + +pub use crate::request_opt::VerifyResult; +use crate::{fetch_as_json_string, log}; +use futures::AsyncWriteExt; +use hyper::{body::to_bytes, Body, Request, StatusCode}; +use js_sys::Array; +use strum::EnumMessage; +use tlsn_core::proof::TlsProof; +use tokio_util::compat::FuturesAsyncReadCompatExt; +use url::Url; +use wasm_bindgen::prelude::*; +use web_sys::{Headers, RequestInit, RequestMode}; + +#[derive(strum_macros::EnumMessage, Debug, Clone, Copy)] +#[allow(dead_code)] +enum ProverPhases { + #[strum(message = "Connect application server with websocket proxy")] + ConnectWsProxy, + #[strum(message = "Build prover config")] + BuildProverConfig, + #[strum(message = "Set up prover")] + SetUpProver, + #[strum(message = "Bind the prover to the server connection")] + BindProverToConnection, + #[strum(message = "Spawn the prover thread")] + SpawnProverThread, + #[strum(message = "Attach the hyper HTTP client to the TLS connection")] + AttachHttpClient, + #[strum(message = "Spawn the HTTP task to be run concurrently")] + SpawnHttpTask, + #[strum(message = "Build request")] + BuildRequest, + #[strum(message = "Start MPC-TLS connection with the server")] + StartMpcConnection, + #[strum(message = "Received response from the server")] + ReceivedResponse, + #[strum(message = "Parsing response from the server")] + ParseResponse, + #[strum(message = "Close the connection to the server")] + CloseConnection, + #[strum(message = "Start notarization")] + StartNotarization, + #[strum(message = "Commit to data")] + Commit, + #[strum(message = "Finalize")] + Finalize, + #[strum(message = "Notarization complete")] + NotarizationComplete, + #[strum(message = "Create Proof")] + CreateProof, +} + +fn log_phase(phase: ProverPhases) { + log!( + "!@# tlsn-js {}: {}", + phase as u8, + phase.get_message().unwrap() + ); +} + +#[wasm_bindgen] +pub async fn prover( + target_url_str: &str, + val: JsValue, + secret_headers: JsValue, + secret_body: JsValue, +) -> Result { + log!("target_url: {}", target_url_str); + let target_url = Url::parse(target_url_str) + .map_err(|e| JsValue::from_str(&format!("Could not parse target_url: {:?}", e)))?; + + log!( + "target_url.host: {}", + target_url + .host() + .ok_or(JsValue::from_str("Could not get target host"))? + ); + let options: RequestOptions = serde_wasm_bindgen::from_value(val) + .map_err(|e| JsValue::from_str(&format!("Could not deserialize options: {:?}", e)))?; + log!("options.notary_url: {}", options.notary_url.as_str()); + + // 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_writer(MakeConsoleWriter); // write events to the console + // let perf_layer = performance_layer() + // .with_details_from_fields(Pretty::default()); + + // tracing_subscriber::registry() + // .with(tracing_subscriber::filter::LevelFilter::DEBUG) + // .with(fmt_layer) + // .with(perf_layer) + // .init(); // Install these as subscribers to tracing events + + // https://github.com/rustwasm/console_error_panic_hook + panic::set_hook(Box::new(console_error_panic_hook::hook)); + + let start_time = Instant::now(); + + /* + * Connect Notary with websocket + */ + + let mut opts = RequestInit::new(); + log!("method: {}", "POST"); + opts.method("POST"); + // opts.method("GET"); + opts.mode(RequestMode::Cors); + + // set headers + let headers = Headers::new() + .map_err(|e| JsValue::from_str(&format!("Could not create headers: {:?}", e)))?; + let notary_url = Url::parse(options.notary_url.as_str()) + .map_err(|e| JsValue::from_str(&format!("Could not parse notary_url: {:?}", e)))?; + let notary_ssl = notary_url.scheme() == "https" || notary_url.scheme() == "wss"; + let notary_host = notary_url.authority(); + + headers + .append("Host", notary_host) + .map_err(|e| JsValue::from_str(&format!("Could not append Host header: {:?}", e)))?; + headers + .append("Content-Type", "application/json") + .map_err(|e| { + JsValue::from_str(&format!("Could not append Content-Type header: {:?}", e)) + })?; + opts.headers(&headers); + + log!("notary_host: {}", notary_host); + // set body + let payload = serde_json::to_string(&NotarizationSessionRequest { + client_type: ClientType::Websocket, + max_transcript_size: Some(options.max_transcript_size), + }) + .map_err(|e| JsValue::from_str(&format!("Could not serialize request: {:?}", e)))?; + opts.body(Some(&JsValue::from_str(&payload))); + + // url + let url = format!( + "{}://{}/session", + if notary_ssl { "https" } else { "http" }, + notary_host + ); + log!("Request: {}", url); + let rust_string = fetch_as_json_string(&url, &opts) + .await + .map_err(|e| JsValue::from_str(&format!("Could not fetch session: {:?}", e)))?; + let notarization_response = + serde_json::from_str::(&rust_string) + .map_err(|e| JsValue::from_str(&format!("Could not deserialize response: {:?}", e)))?; + log!("Response: {}", rust_string); + + log!("Notarization response: {:?}", notarization_response,); + let notary_wss_url = format!( + "{}://{}/notarize?sessionId={}", + if notary_ssl { "wss" } else { "ws" }, + notary_host, + notarization_response.session_id + ); + let (_, notary_ws_stream) = WsMeta::connect(notary_wss_url, None) + .await + .expect_throw("assume the notary ws connection succeeds"); + let notary_ws_stream_into = notary_ws_stream.into_io(); + + log_phase(ProverPhases::BuildProverConfig); + + let target_host = target_url + .host_str() + .ok_or(JsValue::from_str("Could not get target host"))?; + // Basic default prover config + let config = ProverConfig::builder() + .id(notarization_response.session_id) + .server_dns(target_host) + .max_transcript_size(options.max_transcript_size) + .build() + .map_err(|e| JsValue::from_str(&format!("Could not build prover config: {:?}", e)))?; + + // Create a Prover and set it up with the Notary + // This will set up the MPC backend prior to connecting to the server. + log_phase(ProverPhases::SetUpProver); + let prover = Prover::new(config) + .setup(notary_ws_stream_into) + .await + .map_err(|e| JsValue::from_str(&format!("Could not set up prover: {:?}", e)))?; + + /* + Connect Application Server with websocket proxy + */ + log_phase(ProverPhases::ConnectWsProxy); + + let (_, client_ws_stream) = WsMeta::connect(options.websocket_proxy_url, None) + .await + .expect_throw("assume the client ws connection succeeds"); + let client_ws_stream_into = client_ws_stream.into_io(); + + // Bind the Prover to the server connection. + // The returned `mpc_tls_connection` is an MPC TLS connection to the Server: all data written + // to/read from it will be encrypted/decrypted using MPC with the Notary. + log_phase(ProverPhases::BindProverToConnection); + let (mpc_tls_connection, prover_fut) = prover + .connect(client_ws_stream_into) + .await + .map_err(|e| JsValue::from_str(&format!("Could not connect prover: {:?}", e)))?; + + let prover_ctrl = prover_fut.control(); + + log_phase(ProverPhases::SpawnProverThread); + let (prover_sender, prover_receiver) = oneshot::channel(); + let handled_prover_fut = async { + let result = prover_fut.await; + let _ = prover_sender.send(result); + }; + spawn_local(handled_prover_fut); + + // Attach the hyper HTTP client to the TLS connection + log_phase(ProverPhases::AttachHttpClient); + let (mut request_sender, connection) = + hyper::client::conn::handshake(mpc_tls_connection.compat()) + .await + .map_err(|e| JsValue::from_str(&format!("Could not handshake: {:?}", e)))?; + + // Spawn the HTTP task to be run concurrently + log_phase(ProverPhases::SpawnHttpTask); + let (connection_sender, connection_receiver) = oneshot::channel(); + let connection_fut = connection.without_shutdown(); + let handled_connection_fut = async { + let result = connection_fut.await; + let _ = connection_sender.send(result); + }; + spawn_local(handled_connection_fut); + + log_phase(ProverPhases::BuildRequest); + let mut req_with_header = Request::builder() + .uri(target_url_str) + .method(options.method.as_str()); + + for (key, value) in options.headers { + log!("adding header: {} - {}", key.as_str(), value.as_str()); + req_with_header = req_with_header.header(key.as_str(), value.as_str()); + } + + let req_with_body = if options.body.is_empty() { + log!("empty body"); + req_with_header.body(Body::empty()) + } else { + log!("added body - {}", options.body.as_str()); + req_with_header.body(Body::from(options.body)) + }; + + let unwrapped_request = req_with_body + .map_err(|e| JsValue::from_str(&format!("Could not build request: {:?}", e)))?; + + log_phase(ProverPhases::StartMpcConnection); + + // Defer decryption of the response. + prover_ctrl + .defer_decryption() + .await + .map_err(|e| JsValue::from_str(&format!("failed to enable deferred decryption: {}", e)))?; + + // Send the request to the Server and get a response via the MPC TLS connection + let response = request_sender + .send_request(unwrapped_request) + .await + .map_err(|e| JsValue::from_str(&format!("Could not send request: {:?}", e)))?; + + log_phase(ProverPhases::ReceivedResponse); + if response.status() != StatusCode::OK { + return Err(JsValue::from_str(&format!( + "Response status is not OK: {:?}", + response.status() + ))); + } + + log_phase(ProverPhases::ParseResponse); + // Pretty printing :) + let payload = to_bytes(response.into_body()) + .await + .map_err(|e| JsValue::from_str(&format!("Could not get response body: {:?}", e)))? + .to_vec(); + let parsed = serde_json::from_str::(&String::from_utf8_lossy(&payload)) + .map_err(|e| JsValue::from_str(&format!("Could not parse response: {:?}", e)))?; + let response_pretty = serde_json::to_string_pretty(&parsed) + .map_err(|e| JsValue::from_str(&format!("Could not serialize response: {:?}", e)))?; + log!("Response: {}", response_pretty); + + // Close the connection to the server + log_phase(ProverPhases::CloseConnection); + let mut client_socket = connection_receiver + .await + .map_err(|e| { + JsValue::from_str(&format!( + "Could not receive from connection_receiver: {:?}", + e + )) + })? + .map_err(|e| JsValue::from_str(&format!("Could not get TlsConnection: {:?}", e)))? + .io + .into_inner(); + client_socket + .close() + .await + .map_err(|e| JsValue::from_str(&format!("Could not close socket: {:?}", e)))?; + + // The Prover task should be done now, so we can grab it. + log_phase(ProverPhases::StartNotarization); + let prover = prover_receiver + .await + .map_err(|e| { + JsValue::from_str(&format!("Could not receive from prover_receiver: {:?}", e)) + })? + .map_err(|e| JsValue::from_str(&format!("Could not get Prover: {:?}", e)))?; + let mut prover = prover.start_notarize(); + + let secret_headers_vecs = string_list_to_bytes_vec(&secret_headers)?; + let secret_headers_slices: Vec<&[u8]> = secret_headers_vecs + .iter() + .map(|vec| vec.as_slice()) + .collect(); + + // Identify the ranges in the transcript that contain revealed_headers + let (sent_public_ranges, sent_private_ranges) = find_ranges( + prover.sent_transcript().data(), + secret_headers_slices.as_slice(), + ); + + let secret_body_vecs = string_list_to_bytes_vec(&secret_body)?; + let secret_body_slices: Vec<&[u8]> = + secret_body_vecs.iter().map(|vec| vec.as_slice()).collect(); + + // Identify the ranges in the transcript that contain the only data we want to reveal later + let (recv_public_ranges, recv_private_ranges) = find_ranges( + prover.recv_transcript().data(), + secret_body_slices.as_slice(), + ); + + log_phase(ProverPhases::Commit); + + let _recv_len = prover.recv_transcript().data().len(); + + let builder = prover.commitment_builder(); + + // Commit to the outbound and inbound transcript, isolating the data that contain secrets + let sent_pub_commitment_ids = sent_public_ranges + .iter() + .map(|range| { + builder.commit_sent(range).map_err(|e| { + JsValue::from_str(&format!("Error committing sent pub range: {:?}", e)) + }) + }) + .collect::, _>>()?; + + sent_private_ranges.iter().try_for_each(|range| { + builder + .commit_sent(range) + .map_err(|e| { + JsValue::from_str(&format!("Error committing sent private range: {:?}", e)) + }) + .map(|_| ()) + })?; + + let recv_pub_commitment_ids = recv_public_ranges + .iter() + .map(|range| { + builder.commit_recv(range).map_err(|e| { + JsValue::from_str(&format!("Error committing recv public ranges: {:?}", e)) + }) + }) + .collect::, _>>()?; + + recv_private_ranges.iter().try_for_each(|range| { + builder + .commit_recv(range) + .map_err(|e| { + JsValue::from_str(&format!("Error committing recv private range: {:?}", e)) + }) + .map(|_| ()) + })?; + + // Finalize, returning the notarized session + log_phase(ProverPhases::Finalize); + let notarized_session = prover + .finalize() + .await + .map_err(|e| JsValue::from_str(&format!("Error finalizing prover: {:?}", e)))?; + + log_phase(ProverPhases::NotarizationComplete); + + // Create a proof for all committed data in this session + log_phase(ProverPhases::CreateProof); + let session_proof = notarized_session.session_proof(); + + let mut proof_builder = notarized_session.data().build_substrings_proof(); + + // Reveal everything except the redacted stuff (which for the response it's everything except the screen_name) + sent_pub_commitment_ids + .iter() + .chain(recv_pub_commitment_ids.iter()) + .try_for_each(|id| { + proof_builder + .reveal_by_id(*id) + .map_err(|e| JsValue::from_str(&format!("Could not reveal commitment: {:?}", e))) + .map(|_| ()) + })?; + + let substrings_proof = proof_builder + .build() + .map_err(|e| JsValue::from_str(&format!("Could not build proof: {:?}", e)))?; + + let proof = TlsProof { + session: session_proof, + substrings: substrings_proof, + }; + + let res = serde_json::to_string_pretty(&proof) + .map_err(|e| JsValue::from_str(&format!("Could not serialize proof: {:?}", e)))?; + + let duration = start_time.elapsed(); + log!("!@# request took {} seconds", duration.as_secs()); + + Ok(res) +} + +/// Find the ranges of the public and private parts of a sequence. +/// +/// Returns a tuple of `(public, private)` ranges. +fn find_ranges(seq: &[u8], private_seq: &[&[u8]]) -> (Vec>, Vec>) { + let mut private_ranges = Vec::new(); + for s in private_seq { + for (idx, w) in seq.windows(s.len()).enumerate() { + if w == *s { + private_ranges.push(idx..(idx + w.len())); + } + } + } + + let mut sorted_ranges = private_ranges.clone(); + sorted_ranges.sort_by_key(|r| r.start); + + let mut public_ranges = Vec::new(); + let mut last_end = 0; + for r in sorted_ranges { + if r.start > last_end { + public_ranges.push(last_end..r.start); + } + last_end = r.end; + } + + if last_end < seq.len() { + public_ranges.push(last_end..seq.len()); + } + + (public_ranges, private_ranges) +} + +fn string_list_to_bytes_vec(secrets: &JsValue) -> Result>, JsValue> { + let array: Array = Array::from(secrets); + let length = array.length(); + let mut byte_slices: Vec> = Vec::new(); + + for i in 0..length { + let secret_js: JsValue = array.get(i); + let secret_str: String = secret_js + .as_string() + .ok_or(JsValue::from_str("Could not convert secret to string"))?; + let secret_bytes = secret_str.into_bytes(); + byte_slices.push(secret_bytes); + } + Ok(byte_slices) +} diff --git a/wasm/prover/src/verify.rs b/wasm/prover/src/verify.rs new file mode 100644 index 0000000..e663cdb --- /dev/null +++ b/wasm/prover/src/verify.rs @@ -0,0 +1,107 @@ +use wasm_bindgen::prelude::*; + +use crate::request_opt::VerifyResult; + +use elliptic_curve::pkcs8::DecodePublicKey; +use std::time::Duration; +use tlsn_core::proof::{SessionProof, TlsProof}; + +use crate::log; + +#[wasm_bindgen] +pub async fn verify(proof: &str, notary_pubkey_str: &str) -> Result { + // log!("!@# proof {}", proof); + let proof: TlsProof = serde_json::from_str(proof) + .map_err(|e| JsValue::from_str(&format!("Could not deserialize proof: {:?}", e)))?; + + 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; + + log!( + "!@# notary_pubkey {}, {}", + notary_pubkey_str, + notary_pubkey_str.len() + ); + session + .verify_with_default_cert_verifier(get_notary_pubkey(notary_pubkey_str)?) + .map_err(|e| JsValue::from_str(&format!("Session verification failed: {:?}", e)))?; + + let SessionProof { + // The session header that was signed by the Notary is a succinct commitment to the TLS transcript. + header, + // This is the server name, 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) + .map_err(|e| JsValue::from_str(&format!("Could not verify substrings: {:?}", e)))?; + + // Replace the bytes which the Prover chose not to disclose with 'X' + sent.set_redacted(b'X'); + recv.set_redacted(b'X'); + + log!("-------------------------------------------------------------------"); + log!( + "Successfully verified that the bytes below came from a session with {:?} at {}.", + session_info.server_name, + time + ); + log!("Note that the bytes which the Prover chose not to disclose are shown as X."); + log!("Bytes sent:"); + log!( + "{}", + String::from_utf8(sent.data().to_vec()).map_err(|e| JsValue::from_str(&format!( + "Could not convert sent data to string: {:?}", + e + )))? + ); + log!("Bytes received:"); + log!( + "{}", + String::from_utf8(recv.data().to_vec()).map_err(|e| JsValue::from_str(&format!( + "Could not convert recv data to string: {:?}", + e + )))? + ); + log!("-------------------------------------------------------------------"); + + let result = VerifyResult { + server_name: String::from(session_info.server_name.as_str()), + time: header.time(), + sent: String::from_utf8(sent.data().to_vec()).map_err(|e| { + JsValue::from_str(&format!("Could not convert sent data to string: {:?}", e)) + })?, + recv: String::from_utf8(recv.data().to_vec()).map_err(|e| { + JsValue::from_str(&format!("Could not convert recv data to string: {:?}", e)) + })?, + }; + let res = serde_json::to_string_pretty(&result) + .map_err(|e| JsValue::from_str(&format!("Could not serialize result: {:?}", e)))?; + + Ok(res) +} + +#[allow(unused)] +fn print_type_of(_: &T) { + log!("{}", std::any::type_name::()); +} + +/// Returns a Notary pubkey trusted by this Verifier +fn get_notary_pubkey(pubkey: &str) -> Result { + p256::PublicKey::from_public_key_pem(pubkey) + .map_err(|e| JsValue::from_str(&format!("Could not get notary pubkey: {:?}", e))) +}