tlsn
Main tee_dev example for WebSocket notarization with TDX attestation.
Main Example
The main runnable example is crates/examples/tee/ws.rs on the tee_dev branch.
It creates a WebSocket notarization session with tee_attestation: Some(true), runs a TLSN request against a configurable HTTPS target, receives the normal TLSN attestation and secrets, then receives and verifies the extra TDX attestation payload.
The underlying TDX session path is documented in the TDX Notary runbook. For the broader Livy verifiable-compute model, refer to the Livy example model.
/notarize Example: tee_ws
- Purpose: run the WebSocket notarization flow with TEE attestation against the deployed public notary
- Flow:
/session+/notarizeWebSocket notarization flow - Repo path:
crates/examples/tee/ws.rs - Branch:
tee_dev - Prerequisites:
- Rust toolchain
- cloned
livylabs/tlsnrepo - public notary reachable at
https://tlsn.livylabs.xyz - public CA trust available locally, which is why
USE_FIXTURE_CA=false
Public Notary Run
NOTARY_SCHEME=https \
NOTARY_HOST=tlsn.livylabs.xyz \
NOTARY_PORT=443 \
TARGET_HOST=api.weather.gov \
TARGET_PORT=443 \
TARGET_SERVER_NAME=api.weather.gov \
TARGET_URI=/points/37.7749,-122.4194 \
USE_FIXTURE_CA=false \
MAX_SENT_DATA=4096 \
MAX_RECV_DATA=16384 \
cargo run -p tlsn-examples --example tee_wsWhat It Does
- creates a notary session over HTTPS
- sets
teeAttestation: trueon the session request - upgrades to WebSocket notarization
- performs an MPC-TLS fetch against
api.weather.gov - commits the full sent and received transcript
- calls
prover.notarize_with_tee(...) - receives a TDX attestation payload after the TLSN attestation
- decodes the TDX quote JSON
- extracts the TDX report data
- verifies the TDX quote
Configuration
| Env var | Default | Purpose |
|---|---|---|
NOTARY_SCHEME | http | Notary scheme, either http or https. |
NOTARY_HOST | 127.0.0.1 | Notary host. |
NOTARY_PORT | 7047 | Notary port. |
TARGET_HOST | 127.0.0.1 | Target HTTPS server host. |
TARGET_PORT | 4000 | Target HTTPS server port. |
TARGET_SERVER_NAME | test-server.io | TLS server name used by the prover. |
TARGET_URI | /formats/json | Request URI sent to the target server. |
MAX_SENT_DATA | 8192 | TLSN maximum sent data. |
MAX_RECV_DATA | 16384 | TLSN maximum received data. |
USE_FIXTURE_CA | true | Use the repository fixture CA for local target testing. |
Expected Output
- HTTP status line from the target request
TLSN attestation received from notary- pretty-printed TDX attestation JSON
TDX attestation verified successfully- extracted TDX report data
- written artifacts:
ws-test.attestation.tlsnws-test.secrets.tlsn
Notes
- The example sends
teeAttestation: trueto/session; the server stores that flag and emits the extra TDX attestation during/notarize. - The example targets the public HTTPS deployment, not a fixture CA or local test server.
Source
Source: crates/examples/tee/ws.rs
use std::env;
use async_tungstenite::{
tokio::{connect_async, connect_async_with_tls_connector_and_config},
tungstenite::protocol::WebSocketConfig,
};
use attestation_verifier::{
decode_tdx_quote_json, extract_report_data_hex, verify_tdx_quote_json,
};
use futures::{AsyncRead, AsyncWrite};
use http_body_util::{BodyExt as _, Empty, Full};
use hyper::{body::Bytes, Request, StatusCode};
use hyper_tls::HttpsConnector;
use hyper_util::{
client::legacy::{connect::HttpConnector, Builder},
rt::{TokioExecutor, TokioIo},
};
use notary_common::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse};
use tls_core::verify::WebPkiVerifier;
use tls_server_fixture::CA_CERT_DER;
use tlsn_core::{request::RequestConfig, CryptoProvider};
use tlsn_prover::{Prover, ProverConfig};
use tokio_native_tls::native_tls::TlsConnector as NativeTlsConnector;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::info;
use ws_stream_tungstenite::WsStream;
const USER_AGENT: &str = "TLSNotary-WS-TDX-Test/1.0";
const DEFAULT_MAX_SENT_DATA: usize = 1 << 13;
const DEFAULT_MAX_RECV_DATA: usize = 1 << 14;
trait NotaryIo: AsyncRead + AsyncWrite + Send + Unpin {}
impl<T> NotaryIo for T where T: AsyncRead + AsyncWrite + Send + Unpin {}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let notary_scheme = env::var("NOTARY_SCHEME").unwrap_or("http".into());
let notary_host: String = env::var("NOTARY_HOST").unwrap_or("127.0.0.1".into());
let notary_port: u16 = env::var("NOTARY_PORT")
.ok()
.and_then(|port| port.parse().ok())
.unwrap_or(7047);
let target_host: String = env::var("TARGET_HOST").unwrap_or("127.0.0.1".into());
let target_port: u16 = env::var("TARGET_PORT")
.ok()
.and_then(|port| port.parse().ok())
.unwrap_or(4000);
let target_server_name: String =
env::var("TARGET_SERVER_NAME").unwrap_or("test-server.io".into());
let target_uri: String = env::var("TARGET_URI").unwrap_or("/formats/json".into());
let max_sent_data: usize = env::var("MAX_SENT_DATA")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(DEFAULT_MAX_SENT_DATA);
let max_recv_data: usize = env::var("MAX_RECV_DATA")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(DEFAULT_MAX_RECV_DATA);
let use_fixture_ca = env::var("USE_FIXTURE_CA")
.map(|value| value == "true" || value == "1")
.unwrap_or(true);
let session_id =
create_websocket_session(¬ary_scheme, ¬ary_host, notary_port, max_sent_data, max_recv_data)
.await?;
info!(%session_id, "Created websocket notarization session");
let notary_ws_socket =
connect_notary_websocket(¬ary_scheme, ¬ary_host, notary_port, &session_id).await?;
let crypto_provider = build_target_crypto_provider(use_fixture_ca)?;
let prover_config = ProverConfig::builder()
.server_name(target_server_name.as_str())
.protocol_config(
tlsn_common::config::ProtocolConfig::builder()
.max_sent_data(max_sent_data)
.max_recv_data(max_recv_data)
.build()?,
)
.crypto_provider(crypto_provider)
.build()?;
let prover = Prover::new(prover_config).setup(notary_ws_socket).await?;
let client_socket = tokio::net::TcpStream::connect((target_host.as_str(), target_port)).await?;
let (mpc_tls_connection, prover_fut) = prover.connect(client_socket.compat()).await?;
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
let prover_task = tokio::spawn(prover_fut);
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(mpc_tls_connection).await?;
tokio::spawn(connection);
let request = Request::builder()
.uri(target_uri)
.header("Host", target_server_name.as_str())
.header("Accept", "*/*")
.header("Accept-Encoding", "identity")
.header("Connection", "close")
.header("User-Agent", USER_AGENT)
.body(Empty::<Bytes>::new())?;
let response = request_sender.send_request(request).await?;
println!("Application server response: {}", response.status());
assert_eq!(response.status(), StatusCode::OK);
let _ = response.into_body().collect().await?;
let mut prover = prover_task.await??;
let (sent_len, recv_len) = prover.transcript().len();
let mut builder = tlsn_core::transcript::TranscriptCommitConfig::builder(prover.transcript());
builder.commit_sent(&(0..sent_len))?;
builder.commit_recv(&(0..recv_len))?;
let mut request_builder = RequestConfig::builder();
request_builder.transcript_commit(builder.build()?);
let request_config = request_builder.build()?;
#[allow(deprecated)]
let (attestation, secrets, tee_attestation) = prover.notarize_with_tee(&request_config).await?;
println!("TLSN attestation received from notary");
println!(
"TDX attestation packet:\n{}",
serde_json::to_string_pretty(&tee_attestation)?
);
let tdx_attestation_json = serde_json::to_string(&tee_attestation.tdx_attestation)?;
let quote = decode_tdx_quote_json(&tdx_attestation_json)?;
let report_data_hex = extract_report_data_hex("e)?;
verify_tdx_quote_json(&tdx_attestation_json).await?;
println!("TDX attestation verified successfully");
println!("Extracted TDX report data: {report_data_hex}");
tokio::fs::write("ws-test.attestation.tlsn", bincode::serialize(&attestation)?).await?;
tokio::fs::write("ws-test.secrets.tlsn", bincode::serialize(&secrets)?).await?;
prover.close().await?;
Ok(())
}
fn build_target_crypto_provider(
use_fixture_ca: bool,
) -> Result<CryptoProvider, Box<dyn std::error::Error>> {
if !use_fixture_ca {
return Ok(CryptoProvider::default());
}
let mut root_store = tls_core::anchors::RootCertStore::empty();
root_store.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))?;
Ok(CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
})
}
async fn connect_notary_websocket(
notary_scheme: &str,
notary_host: &str,
notary_port: u16,
session_id: &str,
) -> Result<Box<dyn NotaryIo>, Box<dyn std::error::Error>> {
let ws_scheme = websocket_scheme(notary_scheme)?;
let ws_url = format!(
"{ws_scheme}://{notary_host}:{notary_port}/notarize?sessionId={session_id}"
);
if notary_scheme == "https" {
let tls_connector = NativeTlsConnector::builder().build()?;
let (notary_ws_stream, _) = connect_async_with_tls_connector_and_config(
ws_url,
Some(tokio_native_tls::TlsConnector::from(tls_connector)),
Some(WebSocketConfig::default()),
)
.await?;
Ok(Box::new(WsStream::new(notary_ws_stream)))
} else {
let (notary_ws_stream, _) = connect_async(ws_url).await?;
Ok(Box::new(WsStream::new(notary_ws_stream)))
}
}
async fn create_websocket_session(
notary_scheme: &str,
notary_host: &str,
notary_port: u16,
max_sent_data: usize,
max_recv_data: usize,
) -> Result<String, Box<dyn std::error::Error>> {
let payload = serde_json::to_vec(&NotarizationSessionRequest {
client_type: ClientType::Websocket,
max_sent_data: Some(max_sent_data),
max_recv_data: Some(max_recv_data),
tee_attestation: Some(true),
})?;
let session_uri = format!("{notary_scheme}://{notary_host}:{notary_port}/session");
let response = if notary_scheme == "https" {
let tls_connector = NativeTlsConnector::builder().build()?;
let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false);
let https_connector =
HttpsConnector::from((http_connector, tokio_native_tls::TlsConnector::from(tls_connector)));
let client = Builder::new(TokioExecutor::new()).build(https_connector);
client
.request(
Request::builder()
.uri(session_uri)
.method("POST")
.header("Host", notary_host)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(payload)))?,
)
.await?
} else {
let notary_socket = tokio::net::TcpStream::connect((notary_host, notary_port)).await?;
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(TokioIo::new(notary_socket)).await?;
tokio::spawn(async move {
let _ = connection.await;
});
request_sender
.send_request(
Request::builder()
.uri(session_uri)
.method("POST")
.header("Host", notary_host)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(payload)))?,
)
.await?
};
if response.status() != StatusCode::OK {
return Err(format!("Session creation failed with status {}", response.status()).into());
}
let payload = response.into_body().collect().await?.to_bytes();
let session = serde_json::from_slice::<NotarizationSessionResponse>(&payload)?;
Ok(session.session_id)
}
fn websocket_scheme(notary_scheme: &str) -> Result<&'static str, Box<dyn std::error::Error>> {
match notary_scheme {
"http" => Ok("ws"),
"https" => Ok("wss"),
_ => Err(format!("Unsupported NOTARY_SCHEME: {notary_scheme}").into()),
}
}Local Fixture Run
Run a local HTTPS target fixture in another terminal:
PORT=4000 cargo run --release --bin tlsn-server-fixtureThen run the WebSocket TDX example against that fixture:
NOTARY_SCHEME=http \
NOTARY_HOST=127.0.0.1 \
NOTARY_PORT=7047 \
TARGET_HOST=127.0.0.1 \
TARGET_PORT=4000 \
TARGET_SERVER_NAME=test-server.io \
TARGET_URI=/formats/json \
USE_FIXTURE_CA=true \
cargo run --release --example tee_wsFor another HTTPS target, keep the Notary settings and replace the target settings:
NOTARY_SCHEME=http \
NOTARY_HOST=<notary-host> \
NOTARY_PORT=7047 \
TARGET_HOST=<target-host> \
TARGET_PORT=<target-port> \
TARGET_SERVER_NAME=<tls-server-name> \
TARGET_URI=/formats/json \
USE_FIXTURE_CA=false \
cargo run --release --example tee_wsThe example writes:
ws-test.attestation.tlsnws-test.secrets.tlsn
It also prints the TDX attestation JSON, verifies the quote, and prints the extracted report data.
/prove Example Surface
The deployed Livy /api/v1/prove endpoint starts a proof job through the
notary-tee proxy. The proxy runs the TLSN prover internally, talks to the
configured upstream Notary, fetches the requested HTTPS URL, stores the TLSN
artifacts, and returns a job_id.
Source:
- API implementation:
crates/notary/server/notary-tee/proxy.rs - Runnable wrapper:
crates/notary/server/notary-tee/examples/proxy.rs
Start A Proof Job
curl -sS -X POST "https://tlsn.livylabs.xyz/api/v1/prove" \
-H "Content-Type: application/json" \
-d '{
"url": "https://data-api.binance.vision/api/v3/exchangeInfo?symbol=ETHUSDC"
}'Example response shape:
{
"job_id": "3fc3e18cccfb167f6ff9bafddbc6ccd4",
"attestation_hash_hex": "...",
"secrets_hash_hex": "...",
"tdx_attestation": {
"...": "..."
}
}Download The TLSN Artifacts
Use the returned job_id to download the generated TLSN attestation and secrets:
JOB_ID="3fc3e18cccfb167f6ff9bafddbc6ccd4"
curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/${JOB_ID}/attestation" \
--output "attestation-${JOB_ID}.tlsn"
curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/${JOB_ID}/secrets" \
--output "secrets-${JOB_ID}.tlsn"One-Line Downloads
curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/3fc3e18cccfb167f6ff9bafddbc6ccd4/attestation" --output "attestation-3fc3e18cccfb167f6ff9bafddbc6ccd4.tlsn"
curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/3fc3e18cccfb167f6ff9bafddbc6ccd4/secrets" --output "secrets-3fc3e18cccfb167f6ff9bafddbc6ccd4.tlsn"Notes
- The
urlmust be HTTPS. POST /api/v1/proveis exposed by the deployed Livynotary-teeproxy.- This is different from the lower-level
tee_wsexample, which uses/sessionand/notarizedirectly. - The artifact endpoints return raw
.tlsnfiles.