diff --git a/tlsn/examples/Cargo.toml b/tlsn/examples/Cargo.toml index ff8f3a8e9c..a7d1b0cbdd 100644 --- a/tlsn/examples/Cargo.toml +++ b/tlsn/examples/Cargo.toml @@ -8,6 +8,10 @@ publish = false tlsn-prover.workspace = true tlsn-notary.workspace = true tlsn-core.workspace = true +tlsn-tls-core.workspace = true +tlsn-tls-client.workspace = true +notary-server = {version = "0.1.0", git = "https://github.com/tlsnotary/notary-server" } +mpz-core.workspace = true futures.workspace = true tokio = { workspace = true, features = [ @@ -26,6 +30,7 @@ tracing-subscriber.workspace = true hyper = { version = "0.14", features = ["client", "http1"] } p256 = { workspace = true, features = ["ecdsa"] } +elliptic-curve = { version = "0.13.5", features = ["pkcs8"]} webpki-roots.workspace = true async-tls = { version = "0.12", default-features = false, features = [ @@ -40,6 +45,14 @@ rustls-pemfile = { version = "1.0.2" } tokio-rustls = { version = "0.24.1" } dotenv = "0.15.0" +[[example]] +name = "simple_prover" +path = "simple_prover.rs" + +[[example]] +name = "simple_verifier" +path = "simple_verifier.rs" + [[example]] name = "twitter_dm" path = "twitter_dm.rs" diff --git a/tlsn/examples/README.md b/tlsn/examples/README.md new file mode 100644 index 0000000000..d6ecc53015 --- /dev/null +++ b/tlsn/examples/README.md @@ -0,0 +1,18 @@ +This folder contains examples showing how to use the TLSNotary protocol. + +`quick_start.md` shows how to perform a simple notarization. + +`twitter_dm.md` shows how to notarize a Twitter DM. + + +### Starting a notary server + +Before running the examples please make sure that the Notary server is already running. The server can be started with: + +```shell +git clone https://github.com/tlsnotary/notary-server +cd notary-server +cargo run --release +``` + +By default the server will be listening on 127.0.0.1:7047 \ No newline at end of file diff --git a/tlsn/examples/simple_prover.rs b/tlsn/examples/simple_prover.rs new file mode 100644 index 0000000000..4234fc4f53 --- /dev/null +++ b/tlsn/examples/simple_prover.rs @@ -0,0 +1,302 @@ +use eyre::Result; +use futures::AsyncWriteExt; +use hyper::{body::to_bytes, client::conn::Parts, Body, Request, StatusCode}; +use rustls::{Certificate, ClientConfig, RootCertStore}; +use std::{ + fs::File as StdFile, + io::BufReader, + net::{IpAddr, SocketAddr}, + ops::Range, + sync::Arc, +}; +use tokio::{fs::File, io::AsyncWriteExt as _, net::TcpStream}; +use tokio_rustls::{client::TlsStream, TlsConnector}; +use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; +use tracing::debug; + +use notary_server::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse}; +use tlsn_prover::{bind_prover, ProverConfig}; + +const SERVER_DOMAIN: &str = "example.com"; +const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"; + +const NOTARY_HOST: &str = "127.0.0.1"; +const NOTARY_PORT: u16 = 7047; +const NOTARY_CA_CERT_PATH: &str = "./rootCA.crt"; +const MAX_TRANSCRIPT_SIZE: usize = 16384; + +/// Runs a simple Prover which connects to the Notary and notarizes a request/response from +/// example.com. The Prover then generates a proof and writes it to disk. +/// +/// Note that the Notary server must be already listening on NOTARY_HOST:NOTARY_PORT +/// (see README.md "Starting a notary server") +#[tokio::main] +async fn main() { + // Initialize logging + tracing_subscriber::fmt::init(); + + // Establish an encrypted connection with the Notary + let (notary_socket, session_id) = connect_to_notary().await; + println!("Connected to the Notary"); + + // A Prover configuration using the session_id returned by the Notary + let config = ProverConfig::builder() + .id(session_id) + .server_dns(SERVER_DOMAIN) + .max_transcript_size(MAX_TRANSCRIPT_SIZE) + .build() + .unwrap(); + + // Connect to the Server via TCP. This is the TLS client socket. + let client_socket = tokio::net::TcpStream::connect((SERVER_DOMAIN, 443)) + .await + .unwrap(); + + // Bind the Prover to the sockets. + // 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. + let (mpc_tls_connection, prover_fut, notary_fut) = + bind_prover(config, client_socket.compat(), notary_socket.compat()) + .await + .unwrap(); + + // Spawn the Notary connection task and the Prover task to be run concurrently + tokio::spawn(notary_fut); + let prover_task = tokio::spawn(prover_fut); + + // Attach the hyper HTTP client to the MPC TLS connection + let (mut request_sender, connection) = + hyper::client::conn::handshake(mpc_tls_connection.compat()) + .await + .unwrap(); + + // Spawn the HTTP task to be run concurrently + let connection_task = tokio::spawn(connection.without_shutdown()); + + // Build a simple HTTP request with common headers + let request = Request::builder() + .uri(format!("https://{SERVER_DOMAIN}")) + .header("Host", SERVER_DOMAIN) + .header("Accept", "*/*") + // Using "identity" instructs the Server not to use compression for its HTTP response. + // TLSNotary tooling does not support compression. + .header("Accept-Encoding", "identity") + .header("Connection", "close") + .header("User-Agent", USER_AGENT) + .body(Body::empty()) + .unwrap(); + + println!("Starting an MPC TLS connection with the server"); + + // Send the request to the Server and get a response via the MPC TLS connection + let response = request_sender.send_request(request).await.unwrap(); + + println!("Got a response from the server"); + + assert!(response.status() == StatusCode::OK); + + // Close the connection to the server + let mut client_socket = connection_task.await.unwrap().unwrap().io.into_inner(); + client_socket.close().await.unwrap(); + + // The Prover task should be done now, so we can grab the Prover. + let mut prover = prover_task.await.unwrap().unwrap(); + + // Prepare for selective disclosure. + + // Identify the ranges in the outbound data which contain data which we want to disclose + let (public_ranges, _) = find_ranges( + prover.sent_transcript().data(), + &[ + // Redact the value of the "User-Agent" header. It will NOT be disclosed. + USER_AGENT.as_bytes(), + ], + ); + + // Commit to each range of the outbound data which we want to disclose + for range in public_ranges.iter() { + prover.add_commitment_sent(range.clone()).unwrap(); + } + + // Commit to all inbound data in one shot, as we don't need to redact anything in it + let recv_len = prover.recv_transcript().data().len(); + prover.add_commitment_recv(0..recv_len as u32).unwrap(); + + // Finalize, returning the notarized session + let notarized_session = prover.finalize().await.unwrap(); + + // Create a proof for all committed data in this session + let session_proof = notarized_session.session_proof(); + let ids = (0..notarized_session.data().commitments().len()).collect(); + let substrings_proof = notarized_session.generate_substring_proof(ids).unwrap(); + + // Write the proof to a file in the format expected by `simple_verifier.rs` + let mut file = tokio::fs::File::create("proof.json").await.unwrap(); + file.write_all( + serde_json::to_string_pretty(&(&session_proof, &substrings_proof, &SERVER_DOMAIN)) + .unwrap() + .as_bytes(), + ) + .await + .unwrap(); + + println!("Notarization completed successfully!"); + println!("The proof has been written to proof.json"); +} + +/// Connects to the Notary and sets up a notarization session. +/// +/// Returns a socket used to communicate with the Notary and a notarization session id. +async fn connect_to_notary() -> (TlsStream, String) { + // Connect to the Notary via TLS + + // Since the Notary will present a self-signed certificate, we add the CA which signed the + // certificate to the trusted list + let mut certificate_file_reader = read_pem_file(NOTARY_CA_CERT_PATH).await.unwrap(); + let mut certificates: Vec = rustls_pemfile::certs(&mut certificate_file_reader) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); + let certificate = certificates.remove(0); + + let mut root_store = RootCertStore::empty(); + root_store.add(&certificate).unwrap(); + + let client_config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); + let notary_connector = TlsConnector::from(Arc::new(client_config)); + + // Establish a TCP connection to the notary + let notary_socket = tokio::net::TcpStream::connect(SocketAddr::new( + IpAddr::V4(NOTARY_HOST.parse().unwrap()), + NOTARY_PORT, + )) + .await + .unwrap(); + + // Wrap the TCP connection in TLS + // Tell the TLS backend to expect that the Notary's cert was issued to "tlsnotaryserver.io" + let notary_tls_socket = notary_connector + .connect("tlsnotaryserver.io".try_into().unwrap(), notary_socket) + .await + .unwrap(); + + // Attach the hyper HTTP client to the notary TLS connection and start the TLS connection + let (mut request_sender, connection) = hyper::client::conn::handshake(notary_tls_socket) + .await + .unwrap(); + + // Spawn the HTTP task to be run concurrently + let connection_task = tokio::spawn(connection.without_shutdown()); + + // Build the HTTP request to configure notarization + let payload = serde_json::to_string(&NotarizationSessionRequest { + client_type: ClientType::Tcp, + max_transcript_size: Some(MAX_TRANSCRIPT_SIZE), + }) + .unwrap(); + let request = Request::builder() + .uri(format!("https://{NOTARY_HOST}:{NOTARY_PORT}/session")) + .method("POST") + .header("Host", NOTARY_HOST.clone()) + // Need to specify application/json for axum to parse it as json + .header("Content-Type", "application/json") + .body(Body::from(payload)) + .unwrap(); + + debug!("Sending configuration request"); + + let configuration_response = request_sender.send_request(request).await.unwrap(); + + debug!("Sent configuration request"); + + assert!(configuration_response.status() == StatusCode::OK); + + debug!("Response OK"); + + // Pretty printing :) + let configuration_response = to_bytes(configuration_response.into_body()) + .await + .unwrap() + .to_vec(); + let configuration_response = serde_json::from_str::( + &String::from_utf8_lossy(&configuration_response), + ) + .unwrap(); + + debug!("Configuration response: {:?}", configuration_response,); + + // Request the notary to prepare for notarization via HTTP, where the underlying TCP connection + // will be extracted later + let request = Request::builder() + .uri(format!("https://{NOTARY_HOST}:{NOTARY_PORT}/notarize")) + .method("GET") + .header("Host", NOTARY_HOST) + .header("Connection", "Upgrade") + // Need to specify this upgrade header for server to extract tcp connection later + .header("Upgrade", "TCP") + // Need to specify the session_id so that notary server knows the right configuration to use + // as the configuration is set in the previous HTTP call + .header("X-Session-Id", configuration_response.session_id.clone()) + .body(Body::empty()) + .unwrap(); + + debug!("Sending notarization preparation request"); + + let response = request_sender.send_request(request).await.unwrap(); + + debug!("Sent notarization preparation request"); + + assert!(response.status() == StatusCode::SWITCHING_PROTOCOLS); + + debug!("Switched protocol OK"); + + // Claim back the TLS socket after HTTP exchange is done + let Parts { + io: notary_tls_socket, + .. + } = connection_task.await.unwrap().unwrap(); + + (notary_tls_socket, configuration_response.session_id) +} + +/// 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 as u32..(idx + w.len()) as u32); + } + } + } + + 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() as u32 { + public_ranges.push(last_end..seq.len() as u32); + } + + (public_ranges, private_ranges) +} + +/// Read a PEM-formatted file and return its buffer reader +async fn read_pem_file(file_path: &str) -> Result> { + let key_file = File::open(file_path).await?.into_std().await; + Ok(BufReader::new(key_file)) +} diff --git a/tlsn/examples/simple_verifier.rs b/tlsn/examples/simple_verifier.rs new file mode 100644 index 0000000000..dd9a562fed --- /dev/null +++ b/tlsn/examples/simple_verifier.rs @@ -0,0 +1,114 @@ +use elliptic_curve::pkcs8::DecodePublicKey; +use p256::ecdsa::{signature::Verifier, VerifyingKey}; +use std::time::{Duration, UNIX_EPOCH}; + +use mpz_core::serialize::CanonicalSerialize; +use tls_core::{ + anchors::{OwnedTrustAnchor, RootCertStore}, + dns::ServerName, + verify::ServerCertVerifier, +}; +use tlsn_core::{signature::Signature, substrings::proof::SubstringsProof, SessionProof}; + +/// A simple verifier which reads a proof generated by `simple_prover.rs` from "proof.json", verifies +/// it and prints the verified data to the console. +fn main() { + // Deserialize the proof + let proof = std::fs::read_to_string("proof.json").unwrap(); + let (session_proof, substrings_proof, domain): (SessionProof, SubstringsProof, String) = + serde_json::from_str(proof.as_str()).unwrap(); + + // Destructure + let SessionProof { + header, + signature, + handshake_data_decommitment, + } = session_proof; + + // Notary signature type must be correct + #[allow(irrefutable_let_patterns)] + let Signature::P256(signature) = signature.unwrap() else { + panic!("Notary signature is not P256"); + }; + + // Verify the signed header against a trusted Notary's public key + notary_pubkey() + .verify(&header.to_bytes(), &signature) + .unwrap(); + + // Verify the decommitment + handshake_data_decommitment + .verify(header.handshake_summary().handshake_commitment()) + .unwrap(); + + // Verify TLS handshake data. This verifies the server's certificate chain and the server's + // signature against the provided server name. + handshake_data_decommitment + .data() + .verify( + &cert_verifier(), + UNIX_EPOCH + Duration::from_secs(header.handshake_summary().time()), + &ServerName::try_from(domain.as_str()).unwrap(), + ) + .unwrap(); + + // Verify the proof + let (sent_slices, recv_slices) = substrings_proof.verify(&header).unwrap(); + + // Flatten transcript slices into a bytestring, filling the bytes which the Prover chose not + // to disclose with 'X' + let mut transcript_tx = vec![b'X'; header.sent_len() as usize]; + for slice in sent_slices { + transcript_tx[slice.range().start as usize..slice.range().end as usize] + .copy_from_slice(slice.data()) + } + + let mut transcript_rx = vec![b'X'; header.recv_len() as usize]; + for slice in recv_slices { + transcript_rx[slice.range().start as usize..slice.range().end as usize] + .copy_from_slice(slice.data()) + } + + println!("-------------------------------------------------------------------"); + println!( + "Successfully verified that the bytes below came from a session with {:?}.", + domain + ); + 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(transcript_tx).unwrap()); + println!(); + println!("Bytes received:"); + println!(); + println!("{}", String::from_utf8(transcript_rx).unwrap()); + println!("-------------------------------------------------------------------"); +} + +/// Returns a default certificate verifier. +fn cert_verifier() -> impl ServerCertVerifier { + let mut root_store = RootCertStore::empty(); + root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { + OwnedTrustAnchor::from_subject_spki_name_constraints( + ta.subject, + ta.spki, + ta.name_constraints, + ) + })); + + tls_core::verify::WebPkiVerifier::new(root_store, None) +} + +/// Returns a Notary pubkey trusted by this Verifier +fn notary_pubkey() -> VerifyingKey { + // from https://github.com/tlsnotary/notary-server/tree/main/src/fixture/notary/notary.key + // converted with `openssl ec -in notary.key -pubout -outform PEM` + + let pem = "-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBv36FI4ZFszJa0DQFJ3wWCXvVLFr +cRzMG5kaTeHGoSzDu6cFqx3uEWYpFGo6C0EOUgf+mEgbktLrXocv5yHzKg== +-----END PUBLIC KEY-----"; + + VerifyingKey::from_public_key_pem(pem).unwrap() +} diff --git a/tlsn/tlsn-core/src/transcript.rs b/tlsn/tlsn-core/src/transcript.rs index 0162f2940e..9b491e299f 100644 --- a/tlsn/tlsn-core/src/transcript.rs +++ b/tlsn/tlsn-core/src/transcript.rs @@ -121,6 +121,11 @@ mod tests { let range1 = Range { start: 2, end: 4 }; let range2 = Range { start: 10, end: 15 }; + // a full range spanning the entirety of the data + let range3 = Range { + start: 0, + end: sent.data().len() as u32, + }; let expected = "ta12345".as_bytes().to_vec(); assert_eq!( @@ -134,6 +139,8 @@ mod tests { expected, recv.get_bytes_in_ranges(&[range1, range2]).unwrap() ); + + assert_eq!(sent.data(), sent.get_bytes_in_ranges(&[range3]).unwrap()); } #[rstest] @@ -144,8 +151,11 @@ mod tests { let err = sent.get_bytes_in_ranges(&[]); assert_eq!(err.unwrap_err(), Error::InternalError); - // range larger than data length - let bad_range = Range { start: 2, end: 40 }; + // a range with the end bound larger than the data length + let bad_range = Range { + start: 2, + end: (sent.data().len() + 1) as u32, + }; let err = sent.get_bytes_in_ranges(&[bad_range]); assert_eq!(err.unwrap_err(), Error::InternalError); }