Use a local server (fixture) for the attestation example (#656)

feat: use server fixture for tlsn examples + removed Discord example

The attestation example now has different modes: json, html and authenticated
This commit is contained in:
Hendrik Eeckhaut
2024-10-29 13:30:02 +01:00
parent db90e28e44
commit c6dc262a5e
14 changed files with 431 additions and 452 deletions

View File

@@ -12,9 +12,15 @@ tlsn-prover = { workspace = true }
tlsn-utils = { workspace = true }
tlsn-verifier = { workspace = true }
tlsn-formats = { workspace = true }
tlsn-tls-core = { workspace = true }
tls-server-fixture = { workspace = true }
tlsn-server-fixture = { workspace = true }
tlsn-server-fixture-certs = { workspace = true }
spansy = { workspace = true }
bincode = { workspace = true }
chrono = { workspace = true }
clap = { version = "4.5", features = ["derive"] }
dotenv = { version = "0.15.0" }
futures = { workspace = true }
http-body-util = { workspace = true }
@@ -50,7 +56,3 @@ path = "attestation/verify.rs"
[[example]]
name = "interactive"
path = "interactive/interactive.rs"
[[example]]
name = "discord_dm"
path = "discord/discord_dm.rs"

View File

@@ -1,10 +1,8 @@
# Examples
This folder contains examples showing how to use the TLSNotary protocol.
This folder contains examples demonstrating how to use the TLSNotary protocol.
* [attestation](./attestation/README.md) shows how to perform a simple notarization.
* [interactive](./interactive/README.md) interactive Prover and Verifier, without a trusted notary.
* [twitter](./twitter/README.md) shows how to notarize a Twitter DM.
* [discord](./discord/README.md) shows how to notarize a Discord DM.
* [Interactive](./interactive/README.md): Interactive Prover and Verifier session without a trusted notary.
* [Attestation](./attestation/README.md): Performing a simple notarization with a trusted notary.
Refer to <https://docs.tlsnotary.org/quick_start/index.html> for a quick start with TLSNotary using these examples.
Refer to <https://docs.tlsnotary.org/quick_start/index.html> for a quick start guide to using TLSNotary with these examples.

View File

@@ -1,75 +1,110 @@
## Simple Attestation Example: Notarize Public Data from example.com (Rust) <a name="rust-simple"></a>
This example demonstrates the simplest possible use case for TLSNotary:
1. Fetch <https://example.com/> and acquire an attestation of its content.
2. Create a verifiable presentation using the attestation, while redacting the value of a header.
This example demonstrates the simplest possible use case for TLSNotary. A Prover notarizes data from a local test server with a local Notary.
**Overview**:
1. Notarize a request and response from the test server and acquire an attestation of its content.
2. Create a redacted, verifiable presentation using the attestation.
3. Verify the presentation.
### 1. Notarize <https://example.com/>
### 1. Notarize
Run the `prove` binary.
Before starting the notarization, set up the local test server and local notary.
1. Run the test server:
```shell
PORT=4000 cargo run --bin tlsn-server-fixture
```
2. Run the notary server:
```shell
cd crates/notary/server
cargo run -r -- --tls-enabled false
```
3. Run the prove example:
```shell
SERVER_PORT=4000 cargo run --release --example attestation_prove
```
To see more details, run with additional debug information:
```shell
cargo run --release --example attestation_prove
RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example attestation_prove
```
If the notarization was successful, you should see this output in the console:
If notarization is successful, you should see the following output in the console:
```log
Starting an MPC TLS connection with the server
Got a response from the server
Notarization completed successfully!
The attestation has been written to `example.attestation.tlsn` and the corresponding secrets to `example.secrets.tlsn`.
The attestation has been written to `example-json.attestation.tlsn` and the corresponding secrets to `example-json.secrets.tlsn`.
```
⚠️ In this simple example the `Notary` server is automatically started in the background. Note that this is for demonstration purposes only. In a real world example, the notary should be run by a trusted party. Consult the [Notary Server Docs](https://docs.tlsnotary.org/developers/notary_server.html) for more details on how to run a notary server.
⚠️ Note: In this example, we run a local Notary server for demonstration purposes. In real-world applications, the Notary should be operated by a trusted third party. Refer to the [Notary Server Documentation](https://docs.tlsnotary.org/developers/notary_server.html) for more details on running a Notary server.
### 2. Build a verifiable presentation
### 2. Build a Verifiable Presentation
This will build a verifiable presentation with the `User-Agent` header redacted from the request. This presentation can be shared with any verifier you wish to present the data to.
Run the `present` binary.
This step creates a verifiable presentation with optional redactions, which can be shared with any verifier.
Run the present example:
```shell
cargo run --release --example attestation_present
```
If successful, you should see this output in the console:
If successful, youll see this output in the console:
```log
Presentation built successfully!
The presentation has been written to `example.presentation.tlsn`.
The presentation has been written to `example-json.presentation.tlsn`.
```
### 3. Verify the presentation
You can create multiple presentations from the attestation and secrets in the notarization step, each with customized data redactions. You are invited to experiment!
This will read the presentation from the previous step, verify it, and print the disclosed data to console.
### 3. Verify the Presentation
Run the `verify` binary.
This step reads the presentation created above, verifies it, and prints the disclosed data to the console.
Run the verify binary:
```shell
cargo run --release --example attestation_verify
```
If successful, you should see this output in the console:
Upon success, you should see output similar to:
```log
Verifying presentation with {key algorithm} key: { hex encoded key }
**Ask yourself, do you trust this key?**
-------------------------------------------------------------------
Successfully verified that the data below came from a session with example.com at 2024-10-03 03:01:40 UTC.
Successfully verified that the data below came from a session with test-server.io at 2024-10-03 03:01:40 UTC.
Note that the data which the Prover chose not to disclose are shown as X.
Data sent:
...
```
⚠️ Notice that the presentation comes with a "verifying key". This is the key the Notary used when issuing the attestation that the presentation was built from. If you trust the Notary, or more specifically the verifying key, then you can trust that the presented data is authentic.
⚠️ The presentation includes a verifying key,” which the Notary used when issuing the attestation. If you trust this key, you can trust the authenticity of the presented data.
### Next steps
### HTML
Try out the [Discord example](../discord/README.md) and notarize a Discord conversations.
In the example above, we notarized a JSON response. TLSNotary also supports notarizing HTML content. To run an HTML example, use:
```shell
# notarize
SERVER_PORT=4000 cargo run --release --example attestation_prove -- html
# present
cargo run --release --example attestation_present -- html
# verify
cargo run --release --example attestation_verify -- html
```
### Private Data
The examples above demonstrate how to use TLSNotary with publicly accessible data. TLSNotary can also be utilized for private data that requires authentication. To access this data, you can add the necessary headers (such as an authentication token) or cookies to your request. To run an example that uses an authentication token, execute the following command:
```shell
# notarize
SERVER_PORT=4000 cargo run --release --example attestation_prove -- authenticated
# present
cargo run --release --example attestation_present -- authenticated
# verify
cargo run --release --example attestation_verify -- authenticated
```

View File

@@ -2,16 +2,37 @@
// attestation and the corresponding connection secrets. See the `prove.rs`
// example to learn how to acquire an attestation from a Notary.
use hyper::header;
use tlsn_core::{attestation::Attestation, presentation::Presentation, CryptoProvider, Secrets};
use tlsn_examples::ExampleType;
use tlsn_formats::http::HttpTranscript;
fn main() -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// What data to notarize
#[clap(default_value_t, value_enum)]
example_type: ExampleType,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
create_presentation(&args.example_type).await
}
async fn create_presentation(example_type: &ExampleType) -> Result<(), Box<dyn std::error::Error>> {
let attestation_path = tlsn_examples::get_file_path(example_type, "attestation");
let secrets_path = tlsn_examples::get_file_path(example_type, "secrets");
// Read attestation from disk.
let attestation: Attestation =
bincode::deserialize(&std::fs::read("example.attestation.tlsn")?)?;
let attestation: Attestation = bincode::deserialize(&std::fs::read(attestation_path)?)?;
// Read secrets from disk.
let secrets: Secrets = bincode::deserialize(&std::fs::read("example.secrets.tlsn")?)?;
let secrets: Secrets = bincode::deserialize(&std::fs::read(secrets_path)?)?;
// Parse the HTTP transcript.
let transcript = HttpTranscript::parse(secrets.transcript())?;
@@ -24,16 +45,48 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
builder.reveal_sent(&request.without_data())?;
// Reveal the request target.
builder.reveal_sent(&request.request.target)?;
// Reveal all headers except the value of the User-Agent header.
// Reveal all headers except the values of User-Agent and Authorization.
for header in &request.headers {
if !header.name.as_str().eq_ignore_ascii_case("User-Agent") {
if !(header
.name
.as_str()
.eq_ignore_ascii_case(header::USER_AGENT.as_str())
|| header
.name
.as_str()
.eq_ignore_ascii_case(header::AUTHORIZATION.as_str()))
{
builder.reveal_sent(header)?;
} else {
builder.reveal_sent(&header.without_value())?;
}
}
// Reveal the entire response.
builder.reveal_recv(&transcript.responses[0])?;
// Reveal only parts of the response
let response = &transcript.responses[0];
builder.reveal_recv(&response.without_data())?;
for header in &response.headers {
builder.reveal_recv(header)?;
}
let content = &response.body.as_ref().unwrap().content;
match content {
tlsn_formats::http::BodyContent::Json(json) => {
// For experimentation, reveal the entire response or just a selection
let reveal_all = false;
if reveal_all {
builder.reveal_recv(response)?;
} else {
builder.reveal_recv(json.get("id").unwrap())?;
builder.reveal_recv(json.get("information.name").unwrap())?;
builder.reveal_recv(json.get("meta.version").unwrap())?;
}
}
tlsn_formats::http::BodyContent::Unknown(span) => {
builder.reveal_recv(span)?;
}
_ => {}
}
let transcript_proof = builder.build()?;
@@ -48,14 +101,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let presentation: Presentation = builder.build()?;
let presentation_path = tlsn_examples::get_file_path(example_type, "presentation");
// Write the presentation to disk.
std::fs::write(
"example.presentation.tlsn",
bincode::serialize(&presentation)?,
)?;
std::fs::write(&presentation_path, bincode::serialize(&presentation)?)?;
println!("Presentation built successfully!");
println!("The presentation has been written to `example.presentation.tlsn`.");
println!("The presentation has been written to `{presentation_path}`.");
Ok(())
}

View File

@@ -2,49 +2,117 @@
// an HTTP request sent to example.com. The attestation and secrets are saved to
// disk.
use std::env;
use http_body_util::Empty;
use hyper::{body::Bytes, Request, StatusCode};
use hyper_util::rt::TokioIo;
use spansy::Spanned;
use tlsn_examples::ExampleType;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use notary_client::{Accepted, NotarizationRequest, NotaryClient};
use tls_server_fixture::SERVER_DOMAIN;
use tlsn_common::config::ProtocolConfig;
use tlsn_core::{request::RequestConfig, transcript::TranscriptCommitConfig};
use tlsn_examples::run_notary;
use tlsn_formats::http::{DefaultHttpCommitter, HttpCommit, HttpTranscript};
use tlsn_prover::{Prover, ProverConfig};
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
use tracing::debug;
use clap::Parser;
// Setting of the application server
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";
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// What data to notarize
#[clap(default_value_t, value_enum)]
example_type: ExampleType,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let (uri, extra_headers) = match args.example_type {
ExampleType::Json => ("/formats/json", vec![]),
ExampleType::Html => ("/formats/html", vec![]),
ExampleType::Authenticated => ("/protected", vec![("Authorization", "random_auth_token")]),
};
notarize(uri, extra_headers, &args.example_type).await
}
async fn notarize(
uri: &str,
extra_headers: Vec<(&str, &str)>,
example_type: &ExampleType,
) -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let (prover_socket, notary_socket) = tokio::io::duplex(1 << 16);
let notary_host: String = env::var("NOTARY_HOST").unwrap_or("127.0.0.1".into());
let notary_port: u16 = env::var("NOTARY_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(7047);
let server_host: String = env::var("SERVER_HOST").unwrap_or("127.0.0.1".into());
let server_port: u16 = env::var("SERVER_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_FIXTURE_PORT);
// Start a local simple notary service
tokio::spawn(run_notary(notary_socket.compat()));
// 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()
// We must configure the amount of data we expect to exchange beforehand, which will
// be preprocessed prior to the connection. Reducing these limits will improve
// performance.
.max_sent_data(tlsn_examples::MAX_SENT_DATA)
.max_recv_data(tlsn_examples::MAX_RECV_DATA)
.build()?;
let Accepted {
io: notary_connection,
id: _session_id,
..
} = notary_client
.request_notarization(notarization_request)
.await
.expect("Could not connect to notary. Make sure it is running.");
// Set up protocol configuration for prover.
// Prover configuration.
let config = ProverConfig::builder()
let prover_config = ProverConfig::builder()
.server_name(SERVER_DOMAIN)
.protocol_config(
ProtocolConfig::builder()
// We must configure the amount of data we expect to exchange beforehand, which will
// be preprocessed prior to the connection. Reducing these limits will improve
// performance.
.max_sent_data(1024)
.max_recv_data(4096)
.max_sent_data(tlsn_examples::MAX_SENT_DATA)
.max_recv_data(tlsn_examples::MAX_RECV_DATA)
.build()?,
)
.crypto_provider(tlsn_examples::get_crypto_provider_with_server_fixture())
.build()?;
// Create a new prover and perform necessary setup.
let prover = Prover::new(config).setup(prover_socket.compat()).await?;
let prover = Prover::new(prover_config)
.setup(notary_connection.compat())
.await?;
// Open a TCP connection to the server.
let client_socket = tokio::net::TcpStream::connect((SERVER_DOMAIN, 443)).await?;
let client_socket = tokio::net::TcpStream::connect((server_host, server_port)).await?;
// Bind the prover to the server connection.
// The returned `mpc_tls_connection` is an MPC TLS connection to the server: all
@@ -64,23 +132,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tokio::spawn(connection);
// Build a simple HTTP request with common headers
let request = Request::builder()
.uri("/")
let request_builder = Request::builder()
.uri(uri)
.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(Empty::<Bytes>::new())?;
.header("User-Agent", USER_AGENT);
let mut request_builder = request_builder;
for (key, value) in extra_headers {
request_builder = request_builder.header(key, value);
}
let request = request_builder.body(Empty::<Bytes>::new())?;
println!("Starting an MPC TLS connection with the server");
// Send the request to the server and wait for the response.
let response = request_sender.send_request(request).await?;
println!("Got a response from the server");
println!("Got a response from the server: {}", response.status());
assert!(response.status() == StatusCode::OK);
@@ -92,6 +164,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse the HTTP transcript.
let transcript = HttpTranscript::parse(prover.transcript())?;
// dbg!(&transcript);
let body_content = &transcript.responses[0].body.as_ref().unwrap().content;
let body = String::from_utf8_lossy(body_content.span().as_bytes());
match body_content {
tlsn_formats::http::BodyContent::Json(_json) => {
let parsed = serde_json::from_str::<serde_json::Value>(&body)?;
debug!("{}", serde_json::to_string_pretty(&parsed)?);
}
tlsn_formats::http::BodyContent::Unknown(_span) => {
debug!("{}", &body);
}
_ => {}
}
// Commit to the transcript.
let mut builder = TranscriptCommitConfig::builder(prover.transcript());
@@ -101,24 +188,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
prover.transcript_commit(builder.build()?);
// Request an attestation.
let config = RequestConfig::default();
let request_config = RequestConfig::default();
let (attestation, secrets) = prover.finalize(&config).await?;
let (attestation, secrets) = prover.finalize(&request_config).await?;
println!("Notarization complete!");
// Write the attestation to disk.
tokio::fs::write(
"example.attestation.tlsn",
bincode::serialize(&attestation)?,
)
.await?;
let attestation_path = tlsn_examples::get_file_path(example_type, "attestation");
let secrets_path = tlsn_examples::get_file_path(example_type, "secrets");
tokio::fs::write(&attestation_path, bincode::serialize(&attestation)?).await?;
// Write the secrets to disk.
tokio::fs::write("example.secrets.tlsn", bincode::serialize(&secrets)?).await?;
tokio::fs::write(&secrets_path, bincode::serialize(&secrets)?).await?;
println!("Notarization completed successfully!");
println!(
"The attestation has been written to `example.attestation.tlsn` and the \
corresponding secrets to `example.secrets.tlsn`."
"The attestation has been written to `{attestation_path}` and the \
corresponding secrets to `{secrets_path}`."
);
Ok(())

View File

@@ -7,15 +7,33 @@ use std::time::Duration;
use tlsn_core::{
presentation::{Presentation, PresentationOutput},
signing::VerifyingKey,
CryptoProvider,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Read the presentation from disk.
let presentation: Presentation =
bincode::deserialize(&std::fs::read("example.presentation.tlsn")?)?;
use clap::Parser;
use tlsn_examples::ExampleType;
let provider = CryptoProvider::default();
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// What data to notarize
#[clap(default_value_t, value_enum)]
example_type: ExampleType,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
verify_presentation(&args.example_type).await
}
async fn verify_presentation(example_type: &ExampleType) -> Result<(), Box<dyn std::error::Error>> {
// Read the presentation from disk.
let presentation_path = tlsn_examples::get_file_path(example_type, "presentation");
let presentation: Presentation = bincode::deserialize(&std::fs::read(presentation_path)?)?;
let provider = tlsn_examples::get_crypto_provider_with_server_fixture();
let VerifyingKey {
alg,

View File

@@ -1,3 +0,0 @@
USER_AGENT=
AUTHORIZATION=
CHANNEL_ID=

View File

@@ -1,89 +0,0 @@
# Notarize Discord DMs
The `discord_dm.rs` example sets up a TLS connection with Discord and notarizes the requested DMs. The attestation and secrets are saved to disk.
This involves 3 steps:
1. Configure the inputs
2. Start the (local) notary server
3. Notarize
## Inputs
In this tlsn/examples/discord folder, create a `.env` file.
Then in that `.env` file, set the values of the following constants by following the format shown in this [example env file](./.env.example).
| Name | Example | Location |
| ------------- | -------------------------------------------------------------------------------- | --------------------------------------------- |
| USER_AGENT | `Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0` | Look for `User-Agent` in a request headers |
| AUTHORIZATION | `MTE1NDe1Otg4N6NxNjczOTM2OA.GYbUBf.aDtcMUKDOmg6C2kxxFtlFSN1pgdMMBtpHgBBEs` | Look for `Authorization` in a request headers |
| CHANNEL_ID | `1154750485639745567` | URL |
You can obtain these parameters by opening [Discord](https://discord.com/channels/@me) in your browser and accessing the message history you want to notarize. Please note that notarizing only works for short transcripts at the moment, so choose a contact with a short history.
Next, open the **Developer Tools**, go to the **Network** tab, and refresh the page. Then, click on **Search** and type `/api` to filter results to Discord API requests. From there you can copy the needed information into your `.env` as indicated above.
You can find the `CHANNEL_ID` directly in the url:
`https://discord.com/channels/@me/{CHANNEL_ID)`
## Start the notary server
1. Edit the notary server [config file](../../notary/server/config/config.yaml) to turn off TLS so that self-signed certificates can be avoided (⚠️ this is only for local development purposes — TLS must be used in production).
```yaml
tls:
enabled: false
...
```
2. Run the following at the root level of this repository to start the notary server:
```shell
cd crates/notary/server
cargo run --release
```
The notary server will now be running in the background waiting for connections.
For more information on how to configure the `Notary` server, please refer to [this](../../notary/server/README.md#running-the-server).
## Notarize
In this tlsn/examples/discord folder, run the following command:
```sh
RUST_LOG=DEBUG,uid_mux=INFO,yamux=INFO cargo run --release --example discord_dm
```
If everything goes well, you should see output similar to the following:
```log
...
2024-06-26T08:49:47.017439Z DEBUG connect:tls_connection: tls_client_async: handshake complete
2024-06-26T08:49:48.676459Z DEBUG connect:tls_connection: tls_client_async: server closed connection
2024-06-26T08:49:48.676481Z DEBUG connect:commit: tls_mpc::leader: committing to transcript
2024-06-26T08:49:48.676503Z DEBUG connect:tls_connection: tls_client_async: client shutdown
2024-06-26T08:49:48.676466Z DEBUG discord_dm: Sent request
2024-06-26T08:49:48.676550Z DEBUG discord_dm: Request OK
2024-06-26T08:49:48.676598Z DEBUG connect:close_connection: tls_mpc::leader: closing connection
2024-06-26T08:49:48.676613Z DEBUG connect: tls_mpc::leader: leader actor stopped
2024-06-26T08:49:48.676618Z DEBUG discord_dm: [
{
"attachments": [],
...
"channel_id": "1154750485639745567",
...
}
]
2024-06-26T08:49:48.678621Z DEBUG finalize: tlsn_prover::tls::notarize: starting finalization
2024-06-26T08:49:48.680839Z DEBUG finalize: tlsn_prover::tls::notarize: received OT secret
2024-06-26T08:49:50.004432Z INFO finalize:poll{role=Client}:handle_shutdown: uid_mux::yamux: mux connection closed
2024-06-26T08:49:50.004448Z INFO finalize:poll{role=Client}: uid_mux::yamux: connection complete
2024-06-26T08:49:50.004583Z DEBUG discord_dm: Notarization complete!
```
If the transcript was too long, you may encounter the following error. This occurs because there is a default limit of notarization size to 16kB:
```
thread 'tokio-runtime-worker' panicked at 'called `Result::unwrap()` on an `Err` value: IOError(Custom { kind: InvalidData, error: BackendError(DecryptionError("Other: KOSReceiverActor is not setup")) })', /Users/heeckhau/tlsnotary/tlsn/tlsn/tlsn-prover/src/lib.rs:173:50
```
# Verify
See the [`present`](../attestation/present.rs) and [`verify`](../attestation/verify.rs) examples for a demonstration of how to construct a presentation and verify it.

View File

@@ -1,216 +0,0 @@
// 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 std::{env, str};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::debug;
use utils::range::RangeSet;
use tlsn_common::config::ProtocolConfig;
use tlsn_core::{request::RequestConfig, transcript::TranscriptCommitConfig};
use tlsn_prover::{Prover, ProverConfig};
// Setting of the application server
const SERVER_DOMAIN: &str = "discord.com";
// 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;
// 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 channel_id = env::var("CHANNEL_ID").unwrap();
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
.expect("Could not connect to notary. Make sure it is running.");
// Set up protocol configuration for prover.
let protocol_config = ProtocolConfig::builder()
.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_config = ProverConfig::builder()
.server_name(SERVER_DOMAIN)
.protocol_config(protocol_config)
.build()
.unwrap();
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!(
"https://{SERVER_DOMAIN}/api/v9/channels/{channel_id}/messages?limit=2"
))
.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::<Bytes>::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::<serde_json::Value>(&String::from_utf8_lossy(&payload)).unwrap();
debug!("{}", serde_json::to_string_pretty(&parsed).unwrap());
// 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 sent_transcript = prover.transcript().sent();
let recv_transcript = prover.transcript().received();
// Identify the ranges in the outbound data which contain data which we want to
// disclose
let (sent_public_ranges, _) = find_ranges(sent_transcript, &[auth_token.as_bytes()]);
#[allow(clippy::single_range_in_vec_init)]
let recv_public_ranges = RangeSet::from([0..recv_transcript.len()]);
let mut builder = TranscriptCommitConfig::builder(prover.transcript());
// Commit to public ranges
builder.commit_sent(&sent_public_ranges).unwrap();
builder.commit_recv(&recv_public_ranges).unwrap();
let config = builder.build().unwrap();
prover.transcript_commit(config);
// Finalize, returning the notarized session
let request_config = RequestConfig::default();
let (attestation, secrets) = prover.finalize(&request_config).await.unwrap();
debug!("Notarization complete!");
tokio::fs::write(
"discord.attestation.tlsn",
bincode::serialize(&attestation).unwrap(),
)
.await
.unwrap();
tokio::fs::write(
"discord.secrets.tlsn",
bincode::serialize(&secrets).unwrap(),
)
.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]]) -> (RangeSet<usize>, RangeSet<usize>) {
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());
}
(
RangeSet::from(public_ranges),
RangeSet::from(private_ranges),
)
}

View File

@@ -2,4 +2,17 @@
This example demonstrates how to use TLSNotary in a simple interactive session between a Prover and a Verifier. It involves the Verifier first verifying the MPC-TLS session and then confirming the correctness of the data.
Note: In this example, the Prover and the Verifier run on the same machine. In real-world scenarios, the Prover and Verifier would be separate entities.
This example fetches data from a local test server. To start this server, run:
```shell
PORT=4000 cargo run --bin tlsn-server-fixture
```
Next, run the interactive example with:
```shell
SERVER_PORT=4000 cargo run --release --example interactive
```
To view more detailed debug information, use the following command:
```
RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example interactive
```
> Note: In this example, the Prover and Verifier run on the same machine. In real-world scenarios, the Prover and Verifier would typically operate on separate machines.

View File

@@ -1,16 +1,24 @@
use std::{
env,
net::{IpAddr, SocketAddr},
};
use http_body_util::Empty;
use hyper::{body::Bytes, Request, StatusCode, Uri};
use hyper_util::rt::TokioIo;
use tlsn_common::config::{ProtocolConfig, ProtocolConfigValidator};
use tlsn_core::transcript::Idx;
use tlsn_examples::get_crypto_provider_with_server_fixture;
use tlsn_prover::{state::Prove, Prover, ProverConfig};
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
use tlsn_server_fixture_certs::SERVER_DOMAIN;
use tlsn_verifier::{SessionInfo, Verifier, VerifierConfig};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::instrument;
const SECRET: &str = "TLSNotary's private key 🤡";
const SERVER_DOMAIN: &str = "example.com";
// Maximum number of bytes that can be sent from prover to server
const MAX_SENT_DATA: usize = 1 << 12;
@@ -21,13 +29,21 @@ const MAX_RECV_DATA: usize = 1 << 14;
async fn main() {
tracing_subscriber::fmt::init();
let uri = "https://example.com";
let id = "interactive verifier demo";
let server_host: String = env::var("SERVER_HOST").unwrap_or("127.0.0.1".into());
let server_port: u16 = env::var("SERVER_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(DEFAULT_FIXTURE_PORT);
// we use SERVER_DOMAIN here to make sure it matches the domain in the test
// server's certificate
let uri = format!("https://{SERVER_DOMAIN}:{server_port}/formats/html");
let server_ip: IpAddr = server_host.parse().expect("Invalid IP address");
let server_addr = SocketAddr::from((server_ip, server_port));
// Connect prover and verifier.
let (prover_socket, verifier_socket) = tokio::io::duplex(1 << 23);
let prover = prover(prover_socket, uri, id);
let verifier = verifier(verifier_socket, id);
let prover = prover(prover_socket, &server_addr, &uri);
let verifier = verifier(verifier_socket);
let (_, (sent, received, _session_info)) = tokio::join!(prover, verifier);
println!("Successfully verified {}", &uri);
@@ -41,13 +57,12 @@ async fn main() {
#[instrument(skip(verifier_socket))]
async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
verifier_socket: T,
server_addr: &SocketAddr,
uri: &str,
id: &str,
) {
let uri = uri.parse::<Uri>().unwrap();
assert_eq!(uri.scheme().unwrap().as_str(), "https");
let server_domain = uri.authority().unwrap().host();
let server_port = uri.port_u16().unwrap_or(443);
// Create prover and connect to verifier.
//
@@ -62,6 +77,7 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
.build()
.unwrap(),
)
.crypto_provider(get_crypto_provider_with_server_fixture())
.build()
.unwrap(),
)
@@ -70,9 +86,7 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
.unwrap();
// Connect to TLS Server.
let tls_client_socket = tokio::net::TcpStream::connect((server_domain, server_port))
.await
.unwrap();
let tls_client_socket = tokio::net::TcpStream::connect(server_addr).await.unwrap();
// Pass server connection into the prover.
let (mpc_tls_connection, prover_fut) =
@@ -109,10 +123,9 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
// Create proof for the Verifier.
let mut prover = prover_task.await.unwrap().unwrap().start_prove();
let idx_sent = redact_and_reveal_sent_data(&mut prover);
let idx_recv = redact_and_reveal_received_data(&mut prover);
// Reveal parts of the transcript
let idx_sent = revealed_ranges_sent(&mut prover);
let idx_recv = revealed_ranges_received(&mut prover);
prover.prove_transcript(idx_sent, idx_recv).await.unwrap();
// Finalize.
@@ -122,7 +135,6 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
#[instrument(skip(socket))]
async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
socket: T,
id: &str,
) -> (Vec<u8>, Vec<u8>, SessionInfo) {
// Setup Verifier.
let config_validator = ProtocolConfigValidator::builder()
@@ -133,6 +145,7 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
let verifier_config = VerifierConfig::builder()
.protocol_config_validator(config_validator)
.crypto_provider(get_crypto_provider_with_server_fixture())
.build()
.unwrap();
let verifier = Verifier::new(verifier_config);
@@ -141,19 +154,19 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
let (mut partial_transcript, session_info) = verifier.verify(socket.compat()).await.unwrap();
partial_transcript.set_unauthed(0);
// Check sent data: check host.
// Check sent data:
let sent = partial_transcript.sent_unsafe().to_vec();
let sent_data = String::from_utf8(sent.clone()).expect("Verifier expected sent data");
sent_data
.find(SERVER_DOMAIN)
.unwrap_or_else(|| panic!("Verification failed: Expected host {}", SERVER_DOMAIN));
// Check received data: check json and version number.
// Check received data:
let received = partial_transcript.received_unsafe().to_vec();
let response = String::from_utf8(received.clone()).expect("Verifier expected received data");
response
.find("Example Domain")
.expect("Expected valid data from example.com");
.find("Herman Melville")
.unwrap_or_else(|| panic!("Expected valid data from {}", SERVER_DOMAIN));
// Check Session info: server name.
assert_eq!(session_info.server_name.as_str(), SERVER_DOMAIN);
@@ -161,8 +174,8 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
(sent, received, session_info)
}
/// Redacts and reveals received data to the verifier.
fn redact_and_reveal_received_data(prover: &mut Prover<Prove>) -> Idx {
/// Returns the received ranges to be revealed to the verifier.
fn revealed_ranges_received(prover: &mut Prover<Prove>) -> Idx {
let recv_transcript = prover.transcript().received();
let recv_transcript_len = recv_transcript.len();
@@ -170,15 +183,15 @@ fn redact_and_reveal_received_data(prover: &mut Prover<Prove>) -> Idx {
let received_string = String::from_utf8(recv_transcript.to_vec()).unwrap();
// Find the substring "illustrative".
let start = received_string
.find("illustrative")
.expect("Error: The substring 'illustrative' was not found in the received data.");
let end = start + "illustrative".len();
.find("Dick")
.expect("Error: The substring 'Dick' was not found in the received data.");
let end = start + "Dick".len();
Idx::new([0..start, end..recv_transcript_len])
}
/// Redacts and reveals sent data to the verifier.
fn redact_and_reveal_sent_data(prover: &mut Prover<Prove>) -> Idx {
/// Returns the sent ranges to be revealed to the verifier.
fn revealed_ranges_sent(prover: &mut Prover<Prove>) -> Idx {
let sent_transcript = prover.transcript().sent();
let sent_transcript_len = sent_transcript.len();

View File

@@ -1,46 +1,45 @@
use futures::{AsyncRead, AsyncWrite};
use k256::{pkcs8::DecodePrivateKey, SecretKey};
use tlsn_common::config::ProtocolConfigValidator;
use tlsn_core::{attestation::AttestationConfig, signing::SignatureAlgId, CryptoProvider};
use tlsn_verifier::{Verifier, VerifierConfig};
/// The private key used by the Notary for signing attestations.
pub const NOTARY_PRIVATE_KEY: &[u8] = &[1u8; 32];
use std::fmt;
use tls_core::verify::WebPkiVerifier;
use tls_server_fixture::CA_CERT_DER;
use tlsn_core::CryptoProvider;
// Maximum number of bytes that can be sent from prover to server
const MAX_SENT_DATA: usize = 1 << 12;
pub 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;
pub const MAX_RECV_DATA: usize = 1 << 14;
/// Runs a simple Notary with the provided connection to the Prover.
pub async fn run_notary<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(conn: T) {
let pem_data = include_str!("../../notary/server/fixture/notary/notary.key");
let secret_key = SecretKey::from_pkcs8_pem(pem_data).unwrap().to_bytes();
let mut provider = CryptoProvider::default();
provider.signer.set_secp256k1(&secret_key).unwrap();
// Setup the config. Normally a different ID would be generated
// for each notarization.
let config_validator = ProtocolConfigValidator::builder()
.max_sent_data(MAX_SENT_DATA)
.max_recv_data(MAX_RECV_DATA)
.build()
/// crypto provider accepting the server-fixture's self-signed certificate
///
/// This is only required for offline testing with the server-fixture. In
/// production, use `CryptoProvider::default()` instead.
pub fn get_crypto_provider_with_server_fixture() -> CryptoProvider {
// custom root store with server-fixture
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()
.protocol_config_validator(config_validator)
.crypto_provider(provider)
.build()
.unwrap();
let attestation_config = AttestationConfig::builder()
.supported_signature_algs(vec![SignatureAlgId::SECP256K1])
.build()
.unwrap();
Verifier::new(config)
.notarize(conn, &attestation_config)
.await
.unwrap();
CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
}
}
#[derive(clap::ValueEnum, Clone, Default, Debug)]
pub enum ExampleType {
#[default]
Json,
Html,
Authenticated,
}
impl fmt::Display for ExampleType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
pub fn get_file_path(example_type: &ExampleType, content_type: &str) -> String {
let example_type = example_type.to_string().to_ascii_lowercase();
format!("example-{}.{}.tlsn", example_type, content_type)
}

View File

@@ -0,0 +1,28 @@
{
"id": 1234567890,
"information": {
"name": "John Doe",
"address": {
"street": "123 Elm Street",
"city": "Anytown",
"state": "XY",
"postalCode": "12345"
},
"favoriteColors": [
"blue",
"red",
"green",
"yellow"
],
"description": "John is a software engineer. He enjoys hiking, playing video games, and reading books. His favorite book is 'Moby Dick'.",
"education": {
"degree": "Bachelor's in Computer Science",
"school": "Anytown University"
}
},
"meta": {
"createdAt": "2022-01-15T14:52:55Z",
"lastUpdatedAt": "2023-01-12T16:42:10Z",
"version": 1.2
}
}

View File

@@ -26,6 +26,9 @@ use serde_json::Value;
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tower_service::Service;
use axum::{async_trait, extract::FromRequest};
use hyper::header;
use tlsn_server_fixture_certs::*;
pub const DEFAULT_FIXTURE_PORT: u16 = 3000;
@@ -40,6 +43,7 @@ fn app(state: AppState) -> Router {
.route("/bytes", get(bytes))
.route("/formats/json", get(json))
.route("/formats/html", get(html))
.route("/protected", get(protected_route))
.with_state(Arc::new(Mutex::new(state)))
}
@@ -136,6 +140,43 @@ async fn html(
Html(include_str!("data/4kb.html"))
}
struct AuthenticatedUser;
#[async_trait]
impl<B> FromRequest<B> for AuthenticatedUser
where
B: Send,
{
type Rejection = (StatusCode, &'static str);
async fn from_request(
req: axum::extract::Request,
_state: &B,
) -> Result<Self, Self::Rejection> {
// Expected token (hardcoded for simplicity in the demo)
let expected_token = "random_auth_token";
let auth_header = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok());
if let Some(auth_token) = auth_header {
let token = auth_token.trim_start_matches("Bearer ");
if token == expected_token {
return Ok(AuthenticatedUser);
}
}
Err((StatusCode::UNAUTHORIZED, "Invalid or missing token"))
}
}
async fn protected_route(_: AuthenticatedUser) -> Result<Json<Value>, StatusCode> {
get_json_value(include_str!("data/protected_data.json"))
}
#[cfg(test)]
mod tests {
use super::*;