diff --git a/tlsn/examples/health_care/.env.example b/tlsn/examples/health_care/.env.example new file mode 100644 index 0000000000..1aa2732335 --- /dev/null +++ b/tlsn/examples/health_care/.env.example @@ -0,0 +1,2 @@ +USER_AGENT= +AUTHORIZATION= \ No newline at end of file diff --git a/tlsn/examples/health_care/health_care.rs b/tlsn/examples/health_care/health_care.rs new file mode 100644 index 0000000000..b2a50f6673 --- /dev/null +++ b/tlsn/examples/health_care/health_care.rs @@ -0,0 +1,250 @@ +// This example shows how to notarize Discord DMs. +// +// The example uses the notary server implemented in ../../../notary/server + +use http_body_util::{BodyExt, Empty}; +use hyper::{body::Bytes, Request, StatusCode}; +use hyper_util::rt::TokioIo; +use notary_client::{Accepted, NotarizationRequest, NotaryClient}; +use serde::{Deserialize, Serialize}; +use std::{env, ops::Range, str}; +use tlsn_core::proof::TlsProof; +use tlsn_prover::tls::{Prover, ProverConfig}; +use tokio::io::AsyncWriteExt as _; +use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; +use tracing::debug; + +// Setting of the application server +const SERVER_DOMAIN: &str = "myhealthbank.nhi.gov.tw"; +const PAGE_ID: &str = "IHKE3301S01"; + +// Setting of the notary server — make sure these are the same with the config in ../../../notary/server +const NOTARY_HOST: &str = "127.0.0.1"; +const NOTARY_PORT: u16 = 7047; + +// P/S: If the following limits are increased, please ensure max-transcript-size of +// the notary server's config (../../../notary/server) is increased too, where +// max-transcript-size = MAX_SENT_DATA + MAX_RECV_DATA +// +// Maximum number of bytes that can be sent from prover to server +const MAX_SENT_DATA: usize = 1 << 12; +// Maximum number of bytes that can be received by prover from server +const MAX_RECV_DATA: usize = 1 << 14; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // Load secret variables frome environment for discord server connection + dotenv::dotenv().ok(); + let auth_token = env::var("AUTHORIZATION").unwrap(); + let user_agent = env::var("USER_AGENT").unwrap(); + + // Build a client to connect to the notary server. + let notary_client = NotaryClient::builder() + .host(NOTARY_HOST) + .port(NOTARY_PORT) + // WARNING: Always use TLS to connect to notary server, except if notary is running locally + // e.g. this example, hence `enable_tls` is set to False (else it always defaults to True). + .enable_tls(false) + .build() + .unwrap(); + + // Send requests for configuration and notarization to the notary server. + let notarization_request = NotarizationRequest::builder() + .max_sent_data(MAX_SENT_DATA) + .max_recv_data(MAX_RECV_DATA) + .build() + .unwrap(); + + let Accepted { + io: notary_connection, + id: session_id, + .. + } = notary_client + .request_notarization(notarization_request) + .await + .unwrap(); + + // Configure a new prover with the unique session id returned from notary client. + let prover_config = ProverConfig::builder() + .id(session_id) + .server_dns(SERVER_DOMAIN) + .max_sent_data(MAX_SENT_DATA) + .max_recv_data(MAX_RECV_DATA) + .build() + .unwrap(); + + // Create a new prover and set up the MPC backend. + let prover = Prover::new(prover_config) + .setup(notary_connection.compat()) + .await + .unwrap(); + + // Open a new socket to the application server. + let client_socket = tokio::net::TcpStream::connect((SERVER_DOMAIN, 443)) + .await + .unwrap(); + + // Bind the Prover to server connection + let (tls_connection, prover_fut) = prover.connect(client_socket.compat()).await.unwrap(); + + // Spawn the Prover to be run concurrently + let prover_task = tokio::spawn(prover_fut); + + // Attach the hyper HTTP client to the TLS connection + let (mut request_sender, connection) = + hyper::client::conn::http1::handshake(TokioIo::new(tls_connection.compat())) + .await + .unwrap(); + + // Spawn the HTTP task to be run concurrently + tokio::spawn(connection); + + // Build the HTTP request to fetch the DMs + let request = Request::builder() + .uri(format!("/api/ihke3000/{PAGE_ID}/page_load")) + .header("Host", SERVER_DOMAIN) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Accept-Encoding", "identity") + .header("User-Agent", user_agent) + .header("Authorization", &auth_token) + .header("Connection", "close") + .body(Empty::::new()) + .unwrap(); + + debug!("Sending request"); + + let response = request_sender.send_request(request).await.unwrap(); + + debug!("Sent request"); + + assert!(response.status() == StatusCode::OK, "{}", response.status()); + + debug!("Request OK"); + + // Pretty printing :) + let payload = response.into_body().collect().await.unwrap().to_bytes(); + let parsed = + serde_json::from_str::(&String::from_utf8_lossy(&payload)).unwrap(); + debug!("{}", serde_json::to_string_pretty(&parsed).unwrap()); + + let root: Root = serde_json::from_str(&String::from_utf8_lossy(&payload)).unwrap(); + root.sP_IHKE3101S01_Data_1.iter().for_each(|d| { + // personal expense + println!("personal expense: {:?}", d.parT_AMT); + // health insurance point + println!("health insurance point: {:?}", d.appL_DOT); + }); + + // The Prover task should be done now, so we can grab it. + let prover = prover_task.await.unwrap().unwrap(); + + // Prepare for notarization + let mut prover = prover.start_notarize(); + + // Identify the ranges in the transcript that contain secrets + let (public_ranges, private_ranges) = + find_ranges(prover.sent_transcript().data(), &[auth_token.as_bytes()]); + + let recv_len = prover.recv_transcript().data().len(); + + let builder = prover.commitment_builder(); + + // Collect commitment ids for the outbound transcript + let mut commitment_ids = public_ranges + .iter() + .chain(private_ranges.iter()) + .map(|range| builder.commit_sent(range).unwrap()) + .collect::>(); + + // Commit to the full received transcript in one shot, as we don't need to redact anything + commitment_ids.push(builder.commit_recv(&(0..recv_len)).unwrap()); + + // Finalize, returning the notarized session + let notarized_session = prover.finalize().await.unwrap(); + + debug!("Notarization complete!"); + + // Dump the notarized session to a file + let mut file = tokio::fs::File::create("health_care_notarized_session.json") + .await + .unwrap(); + file.write_all( + serde_json::to_string_pretty(¬arized_session) + .unwrap() + .as_bytes(), + ) + .await + .unwrap(); + + let session_proof = notarized_session.session_proof(); + + let mut proof_builder = notarized_session.data().build_substrings_proof(); + + // Reveal everything but the auth token (which was assigned commitment id 2) + proof_builder.reveal_by_id(commitment_ids[0]).unwrap(); + proof_builder.reveal_by_id(commitment_ids[1]).unwrap(); + proof_builder.reveal_by_id(commitment_ids[3]).unwrap(); + + let substrings_proof = proof_builder.build().unwrap(); + + let proof = TlsProof { + session: session_proof, + substrings: substrings_proof, + }; + + // Dump the proof to a file. + let mut file = tokio::fs::File::create("health_care_proof.json") + .await + .unwrap(); + file.write_all(serde_json::to_string_pretty(&proof).unwrap().as_bytes()) + .await + .unwrap(); +} + +/// Find the ranges of the public and private parts of a sequence. +/// +/// Returns a tuple of `(public, private)` ranges. +fn find_ranges(seq: &[u8], sub_seq: &[&[u8]]) -> (Vec>, Vec>) { + let mut private_ranges = Vec::new(); + for s in sub_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) +} + +#[derive(Serialize, Deserialize, Debug)] +struct DataItem { + appL_DOT: u32, + cN_T: u32, + feE_Y: u32, + parT_AMT: u32, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Root { + sP_IHKE3101S01_Data_1: Vec, +} diff --git a/tlsn/examples/health_care/health_care_verifier.rs b/tlsn/examples/health_care/health_care_verifier.rs new file mode 100644 index 0000000000..88be1534c6 --- /dev/null +++ b/tlsn/examples/health_care/health_care_verifier.rs @@ -0,0 +1,76 @@ +use std::{str, time::Duration}; + +use elliptic_curve::pkcs8::DecodePublicKey; + +use tlsn_core::proof::{SessionProof, TlsProof}; + +/// A simple verifier which reads a proof generated by `health_care.rs` from "health_care_proof.json", verifies +/// it and prints the verified data to the console. +fn main() { + // Deserialize the proof + let proof = std::fs::read_to_string("health_care_proof.json").unwrap(); + let proof: TlsProof = serde_json::from_str(proof.as_str()).unwrap(); + + 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()) + .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'); + + println!("-------------------------------------------------------------------"); + println!( + "Successfully verified that the bytes below came from a session with {:?} at {}.", + session_info.server_name, time + ); + println!("Note that the bytes which the Prover chose not to disclose are shown as X."); + println!(); + println!("Bytes sent:"); + println!(); + print!("{}", String::from_utf8(sent.data().to_vec()).unwrap()); + println!(); + println!("Bytes received:"); + println!(); + println!("{}", String::from_utf8(recv.data().to_vec()).unwrap()); + println!("-------------------------------------------------------------------"); +} + +/// Returns a Notary pubkey trusted by this Verifier +fn notary_pubkey() -> p256::PublicKey { + let pem_file = str::from_utf8(include_bytes!( + "../../../notary/server/fixture/notary/notary.pub" + )) + .unwrap(); + p256::PublicKey::from_public_key_pem(pem_file).unwrap() +}