Compare commits

..

16 Commits

Author SHA1 Message Date
Hendrik Eeckhaut
57e6b6935a build: remove old getrandom dependency from wasm build 2025-08-06 09:10:26 -07:00
yuroitaki
2f072b2578 chore: remove notary crates (#953)
Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-08-04 10:41:45 +02:00
sinu.eth
33153d1124 refactor: move web-spawn under web feature (#949)
* refactor: move web-spawn under web feature

* add arch conditional
2025-07-29 07:11:16 -07:00
Hendrik Eeckhaut
2d399d5e24 chore: Update latency/bandwidth plots for new harness (#923)
* Updated latency/bandwidth plots for new harness
* Fix harness Docker build
2025-07-23 10:58:46 +02:00
dan
b6d7249b6d fix(harness): restore multithreading for browser benches (#943) 2025-07-23 05:40:58 +00:00
dan
2a8c1c3382 fix(harness): add custom wasm-pack to build script (#940) 2025-07-22 06:29:12 +00:00
dan
7c27162875 fix(harness): pull latest docker images (#941) 2025-07-21 06:07:48 +00:00
sinu.eth
eef813712d refactor: extract attestation functionality into dedicated crate (#936)
* refactor: extract attestation functionality into dedicated crate

* commit lock

* fix integration test

* clippy

* fix docs

* fix import

* fix wasm types

* fix doctest

* verifier config default rootstore

* fix integration test

* fix notary integration tests
2025-07-09 09:54:11 -07:00
sinu.eth
2e94e08fa6 build(wasm): enable simd128 feature (#937) 2025-07-04 11:22:56 -07:00
dan
97d9475335 fix(harness): do not close connection too early (#935) 2025-07-02 09:40:52 -07:00
sinu.eth
38820d6a3f refactor: consolidate into tlsn crate (#934)
* refactor: consolidate into tlsn crate

* clean up dead code

* bump lock file

* rustfmt

* fix examples

* fix docs script

* clippy

* clippy
2025-07-02 09:40:28 -07:00
sinu.eth
af85fa100f build(wasm): add wasm profile and optimize for perf (#933) 2025-07-02 08:52:47 -07:00
Hendrik Eeckhaut
008b901913 ci: docker image for new harness
* update Docker for new harness
* disable shm in Chrome
2025-06-27 17:58:12 +01:00
Hendrik Eeckhaut
db85f68328 build: update Rust to version 1.88.0 2025-06-27 16:40:29 +01:00
Hendrik Eeckhaut
fb80aa4cc9 chore: Set version number to 0.1.0-alpha.13-pre (#931) 2025-06-20 14:41:33 +02:00
Hendrik Eeckhaut
8dae57d6a7 ci: fix problem with multiple tlsn-wasm build artefacts (#930) 2025-06-20 10:57:35 +02:00
221 changed files with 3237 additions and 13617 deletions

View File

@@ -1,10 +0,0 @@
[target.wasm32-unknown-unknown]
rustflags = [
"-C",
"target-feature=+atomics,+bulk-memory,+mutable-globals",
"-A",
"unused_qualifications"
]
[unstable]
build-std = ["panic_abort", "std"]

View File

@@ -21,17 +21,21 @@ jobs:
- name: Build Docker Image
run: |
docker build -t tlsn-bench . -f ./crates/benches/binary/benches.Dockerfile --build-arg BENCH_TYPE=${{ github.event.inputs.bench_type }}
docker build -t tlsn-bench . -f ./crates/harness/harness.Dockerfile
- name: Run Benchmarks
run: |
docker run --privileged -v ${{ github.workspace }}/crates/benches/binary:/benches tlsn-bench
docker run --privileged -v ./crates/harness/:/benches tlsn-bench bash -c "runner setup; runner --target ${{ github.event.inputs.bench_type }} bench"
- name: Plot Benchmarks
run: |
docker run -v ./crates/harness/:/benches tlsn-bench bash -c "tlsn-harness-plot /benches/bench.toml /benches/metrics.csv --min-max-band --prover-kind ${{ github.event.inputs.bench_type }}"
- name: Upload graphs
uses: actions/upload-artifact@v4
with:
name: benchmark_graphs
path: |
./crates/benches/binary/runtime_vs_latency.html
./crates/benches/binary/runtime_vs_bandwidth.html
./crates/benches/binary/download_size_vs_memory.html
./crates/harness/metrics.csv
./crates/harness/bench.toml
./crates/harness/runtime_vs_latency.html
./crates/harness/runtime_vs_bandwidth.html

View File

@@ -11,7 +11,6 @@ on:
permissions:
id-token: write
contents: read
attestations: write
env:
CARGO_TERM_COLOR: always
@@ -22,8 +21,7 @@ env:
# - https://github.com/privacy-scaling-explorations/mpz/issues/178
# 32 seems to be big enough for the foreseeable future
RAYON_NUM_THREADS: 32
GIT_COMMIT_HASH: ${{ github.event.pull_request.head.sha || github.sha }}
RUST_VERSION: 1.87.0
RUST_VERSION: 1.88.0
jobs:
clippy:
@@ -112,7 +110,8 @@ jobs:
sudo apt-get install -y chromium-chromedriver
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# we install a specific version which supports custom profiles
run: cargo install --git https://github.com/rustwasm/wasm-pack.git --rev 32e52ca
- name: Use caching
uses: Swatinem/rust-cache@v2.7.7
@@ -158,9 +157,6 @@ jobs:
- name: Use caching
uses: Swatinem/rust-cache@v2.7.7
- name: Add custom DNS entry to /etc/hosts for notary TLS test
run: echo "127.0.0.1 tlsnotaryserver.io" | sudo tee -a /etc/hosts
- name: Run integration tests
run: cargo test --locked --profile tests-integration --workspace --exclude tlsn-tls-client --exclude tlsn-tls-core --no-fail-fast -- --include-ignored
@@ -185,201 +181,9 @@ jobs:
files: lcov.info
fail_ci_if_error: true
build-sgx:
runs-on: ubuntu-latest
needs: build-and-test
container:
image: rust:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Clang
run: |
apt update
apt install -y clang
- name: Use caching
uses: Swatinem/rust-cache@v2.7.7
- name: Build Rust Binary
run: |
cargo build --locked --bin notary-server --release --features tee_quote
cp --verbose target/release/notary-server $GITHUB_WORKSPACE
- name: Upload Binary for use in the Gramine Job
uses: actions/upload-artifact@v4
with:
name: notary-server
path: notary-server
if-no-files-found: error
gramine-sgx:
runs-on: ubuntu-latest
needs: build-sgx
container:
image: gramineproject/gramine:latest
if: github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Restore SGX signing key from secrets
run: |
mkdir -p "${HOME}/.config/gramine/"
echo "${{ secrets.SGX_SIGNING_KEY }}" > "${HOME}/.config/gramine/enclave-key.pem"
# verify key
openssl rsa -in "${HOME}/.config/gramine/enclave-key.pem" -check -noout
- name: Download notary-server binary from build job
uses: actions/download-artifact@v4
with:
name: notary-server
path: crates/notary/server/tee
- name: Install jq
run: |
apt update
apt install -y jq
- name: Use Gramine to calculate measurements
run: |
cd crates/notary/server/tee
chmod +x notary-server
gramine-manifest \
-Dlog_level=debug \
-Darch_libdir=/lib/x86_64-linux-gnu \
-Dself_exe=notary-server \
notary-server.manifest.template \
notary-server.manifest
gramine-sgx-sign \
--manifest notary-server.manifest \
--output notary-server.manifest.sgx
gramine-sgx-sigstruct-view --verbose --output-format=json notary-server.sig | tee >> notary-server-sigstruct.json
cat notary-server-sigstruct.json
mr_enclave=$(jq -r '.mr_enclave' notary-server-sigstruct.json)
mr_signer=$(jq -r '.mr_signer' notary-server-sigstruct.json)
echo "mrenclave=$mr_enclave" >>"$GITHUB_OUTPUT"
echo "#### sgx mrenclave" | tee >>$GITHUB_STEP_SUMMARY
echo "\`\`\`mr_enclave: ${mr_enclave}\`\`\`" | tee >>$GITHUB_STEP_SUMMARY
echo "\`\`\`mr_signer: ${mr_signer}\`\`\`" | tee >>$GITHUB_STEP_SUMMARY
- name: Upload notary-server and signatures
id: upload-notary-server-sgx
uses: actions/upload-artifact@v4
with:
name: notary-server-sgx.zip
path: |
crates/notary/server/tee/notary-server
crates/notary/server/tee/notary-server-sigstruct.json
crates/notary/server/tee/notary-server.sig
crates/notary/server/tee/notary-server.manifest
crates/notary/server/tee/notary-server.manifest.sgx
crates/notary/server/tee/README.md
if-no-files-found: error
- name: Attest Build Provenance
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/dev'
uses: actions/attest-build-provenance@v2
with:
subject-name: notary-server-sgx.zip
subject-digest: sha256:${{ steps.upload-notary-server-sgx.outputs.artifact-digest }}
- uses: geekyeggo/delete-artifact@v5 # Delete notary-server from the build job, It is part of the zipfile with the signature
with:
name: notary-server
gramine-sgx-docker:
runs-on: ubuntu-latest
needs: gramine-sgx
permissions:
contents: read
packages: write
env:
CONTAINER_REGISTRY: ghcr.io
if: github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
sparse-checkout: './crates/notary/server/tee/notary-server-sgx.Dockerfile'
- name: Download notary-server-sgx.zip from gramine-sgx job
uses: actions/download-artifact@v4
with:
name: notary-server-sgx.zip
path: ./notary-server-sgx
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.CONTAINER_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker image of notary server
id: meta-notary-server-sgx
uses: docker/metadata-action@v4
with:
images: ${{ env.CONTAINER_REGISTRY }}/${{ github.repository }}/notary-server-sgx
- name: Build and push Docker image of notary server
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta-notary-server-sgx.outputs.tags }}
labels: ${{ steps.meta-notary-server-sgx.outputs.labels }}
file: ./crates/notary/server/tee/notary-server-sgx.Dockerfile
build_and_publish_notary_server_image:
name: Build and publish notary server's image
runs-on: ubuntu-latest
needs: build-and-test
permissions:
contents: read
packages: write
env:
CONTAINER_REGISTRY: ghcr.io
if: github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.CONTAINER_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker image of notary server
id: meta-notary-server
uses: docker/metadata-action@v4
with:
images: ${{ env.CONTAINER_REGISTRY }}/${{ github.repository }}/notary-server
- name: Build and push Docker image of notary server
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta-notary-server.outputs.tags }}
labels: ${{ steps.meta-notary-server.outputs.labels }}
file: ./crates/notary/server/notary-server.Dockerfile
create-release-draft:
name: Create Release Draft
needs: build_and_publish_notary_server_image
needs: build-and-test
runs-on: ubuntu-latest
permissions:
contents: write

View File

@@ -6,7 +6,7 @@ on:
tag:
description: 'Tag to publish to NPM'
required: true
default: 'v0.1.0-alpha.12'
default: 'v0.1.0-alpha.13-pre'
jobs:
release:
@@ -22,7 +22,7 @@ jobs:
RUN_ID=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/tlsnotary/tlsn/actions/workflows/ci.yml/runs?per_page=100" \
--jq '.workflow_runs[] | select(.head_branch == "${{ github.event.inputs.tag }}") | .id')
--jq '.workflow_runs[] | select(.head_branch == "${{ github.event.inputs.tag }}") | .id' | sort | tail -1)
if [ -z "$RUN_ID" ]; then
echo "No run found for tag ${{ github.event.inputs.tag }}"

3091
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[workspace]
members = [
"crates/common",
"crates/attestation",
"crates/components/deap",
"crates/components/cipher",
"crates/components/hmac-sha256",
@@ -9,11 +9,6 @@ members = [
"crates/data-fixtures",
"crates/examples",
"crates/formats",
"crates/notary/client",
"crates/notary/common",
"crates/notary/server",
"crates/notary/tests-integration",
"crates/prover",
"crates/server-fixture/certs",
"crates/server-fixture/server",
"crates/tls/backend",
@@ -22,11 +17,12 @@ members = [
"crates/tls/core",
"crates/mpc-tls",
"crates/tls/server-fixture",
"crates/verifier",
"crates/wasm",
"crates/harness/core",
"crates/harness/executor",
"crates/harness/runner",
"crates/harness/plot",
"crates/tlsn",
]
resolver = "2"
@@ -40,19 +36,14 @@ resolver = "2"
inherits = "release"
opt-level = 1
[profile.release.package."tlsn-wasm"]
opt-level = "z"
[profile.dev.package."tlsn-wasm"]
debug = false
[profile.wasm]
inherits = "release"
lto = true
[workspace.dependencies]
notary-client = { path = "crates/notary/client" }
notary-common = { path = "crates/notary/common" }
notary-server = { path = "crates/notary/server" }
tls-server-fixture = { path = "crates/tls/server-fixture" }
tlsn-attestation = { path = "crates/attestation" }
tlsn-cipher = { path = "crates/components/cipher" }
tlsn-common = { path = "crates/common" }
tlsn-core = { path = "crates/core" }
tlsn-data-fixtures = { path = "crates/data-fixtures" }
tlsn-deap = { path = "crates/components/deap" }
@@ -60,7 +51,6 @@ tlsn-formats = { path = "crates/formats" }
tlsn-hmac-sha256 = { path = "crates/components/hmac-sha256" }
tlsn-key-exchange = { path = "crates/components/key-exchange" }
tlsn-mpc-tls = { path = "crates/mpc-tls" }
tlsn-prover = { path = "crates/prover" }
tlsn-server-fixture = { path = "crates/server-fixture/server" }
tlsn-server-fixture-certs = { path = "crates/server-fixture/certs" }
tlsn-tls-backend = { path = "crates/tls/backend" }
@@ -72,7 +62,7 @@ tlsn-harness-core = { path = "crates/harness/core" }
tlsn-harness-executor = { path = "crates/harness/executor" }
tlsn-harness-runner = { path = "crates/harness/runner" }
tlsn-wasm = { path = "crates/wasm" }
tlsn-verifier = { path = "crates/verifier" }
tlsn = { path = "crates/tlsn" }
mpz-circuits = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
mpz-memory-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
@@ -98,7 +88,6 @@ aes = { version = "0.8" }
aes-gcm = { version = "0.9" }
anyhow = { version = "1.0" }
async-trait = { version = "0.1" }
async-tungstenite = { version = "0.28.2" }
axum = { version = "0.8" }
bcs = { version = "0.1" }
bincode = { version = "1.3" }
@@ -119,12 +108,10 @@ enum-try-as-inner = { version = "0.1" }
env_logger = { version = "0.10" }
futures = { version = "0.3" }
futures-rustls = { version = "0.26" }
futures-util = { version = "0.3" }
generic-array = { version = "0.14" }
ghash = { version = "0.5" }
hex = { version = "0.4" }
hmac = { version = "0.12" }
http = { version = "1.1" }
http-body-util = { version = "0.1" }
hyper = { version = "1.1" }
hyper-util = { version = "0.1" }
@@ -137,7 +124,6 @@ log = { version = "0.4" }
once_cell = { version = "1.19" }
opaque-debug = { version = "0.3" }
p256 = { version = "0.13" }
pkcs8 = { version = "0.10" }
pin-project-lite = { version = "0.2" }
pollster = { version = "0.4" }
rand = { version = "0.9" }
@@ -152,29 +138,26 @@ rstest = { version = "0.17" }
rustls = { version = "0.21" }
rustls-pemfile = { version = "1.0" }
sct = { version = "0.7" }
semver = { version = "1.0" }
serde = { version = "1.0" }
serde_json = { version = "1.0" }
sha2 = { version = "0.10" }
signature = { version = "2.2" }
thiserror = { version = "1.0" }
tiny-keccak = { version = "2.0" }
tokio = { version = "1.38" }
tokio-rustls = { version = "0.24" }
tokio-util = { version = "0.7" }
toml = { version = "0.8" }
tower = { version = "0.5" }
tower-http = { version = "0.5" }
tower-service = { version = "0.3" }
tower-util = { version = "0.3.1" }
tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3" }
uuid = { version = "1.4" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
web-spawn = { version = "0.2" }
web-time = { version = "0.2" }
webpki = { version = "0.22" }
webpki-roots = { version = "0.26" }
ws_stream_tungstenite = { version = "0.14" }
# Use the patched ws_stream_wasm to fix the issue https://github.com/najamelan/ws_stream_wasm/issues/12#issuecomment-1711902958
ws_stream_wasm = { git = "https://github.com/tlsnotary/ws_stream_wasm", rev = "2ed12aad9f0236e5321f577672f309920b2aef51" }
zeroize = { version = "1.8" }

View File

@@ -12,7 +12,7 @@
[actions-url]: https://github.com/tlsnotary/tlsn/actions?query=workflow%3Aci+branch%3Adev
[Website](https://tlsnotary.org) |
[Documentation](https://docs.tlsnotary.org) |
[Documentation](https://tlsnotary.org/docs/intro) |
[API Docs](https://tlsnotary.github.io/tlsn) |
[Discord](https://discord.gg/9XwESXtcN7)
@@ -44,12 +44,9 @@ at your option.
## Directory
- [examples](./crates/examples/): Examples on how to use the TLSNotary protocol.
- [tlsn-prover](./crates/prover/): The library for the prover component.
- [tlsn-verifier](./crates/verifier/): The library for the verifier component.
- [notary](./crates/notary/): Implements the [notary server](https://docs.tlsnotary.org/intro.html#tls-verification-with-a-general-purpose-notary) and its client.
- [components](./crates/components/): Houses low-level libraries.
- [tlsn](./crates/tlsn/): The TLSNotary library.
This repository contains the source code for the Rust implementation of the TLSNotary protocol. For additional tools and implementations related to TLSNotary, visit <https://github.com/tlsnotary>. This includes repositories such as [`tlsn-js`](https://github.com/tlsnotary/tlsn-js), [`tlsn-extension`](https://github.com/tlsnotary/tlsn-extension), [`explorer`](https://github.com/tlsnotary/explorer), among others.
This repository contains the source code for the Rust implementation of the TLSNotary protocol. For additional tools and implementations related to TLSNotary, visit <https://github.com/tlsnotary>. This includes repositories such as [`tlsn-js`](https://github.com/tlsnotary/tlsn-js), [`tlsn-extension`](https://github.com/tlsnotary/tlsn-extension), among others.
## Development

View File

@@ -0,0 +1,40 @@
[package]
name = "tlsn-attestation"
version = "0.1.0-alpha.13-pre"
edition = "2024"
[features]
default = []
fixtures = ["tlsn-core/fixtures", "dep:tlsn-data-fixtures"]
[dependencies]
tlsn-tls-core = { workspace = true }
tlsn-core = { workspace = true }
tlsn-data-fixtures = { workspace = true, optional = true }
bcs = { workspace = true }
blake3 = { workspace = true }
p256 = { workspace = true, features = ["serde"] }
k256 = { workspace = true }
opaque-debug = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
tiny-keccak = { workspace = true, features = ["keccak"] }
webpki-roots = { workspace = true }
[dev-dependencies]
alloy-primitives = { version = "0.8.22", default-features = false }
alloy-signer = { version = "0.12", default-features = false }
alloy-signer-local = { version = "0.12", default-features = false }
rand06-compat = { workspace = true }
rstest = { workspace = true }
tlsn-core = { workspace = true, features = ["fixtures"] }
tlsn-data-fixtures = { workspace = true }
[lints]
workspace = true
[[test]]
name = "api"
required-features = ["fixtures"]

View File

@@ -1,19 +1,17 @@
use std::error::Error;
use rand::{rng, Rng};
use rand::{Rng, rng};
use crate::{
attestation::{
Attestation, AttestationConfig, Body, Extension, FieldId, Header, ServerCertCommitment,
VERSION,
},
use tlsn_core::{
connection::{ConnectionInfo, ServerEphemKey},
hash::HashAlgId,
request::Request,
serialize::CanonicalSerialize,
signing::SignatureAlgId,
transcript::TranscriptCommitment,
CryptoProvider,
};
use crate::{
Attestation, AttestationConfig, Body, CryptoProvider, Extension, FieldId, Header,
ServerCertCommitment, VERSION, request::Request, serialize::CanonicalSerialize,
signing::SignatureAlgId,
};
/// Attestation builder state for accepting a request.
@@ -233,7 +231,7 @@ impl std::fmt::Display for AttestationBuilderError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())
@@ -243,14 +241,15 @@ impl std::fmt::Display for AttestationBuilderError {
#[cfg(test)]
mod test {
use rstest::{fixture, rstest};
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
use crate::{
use tlsn_core::{
connection::{HandshakeData, HandshakeDataV1_2},
fixtures::{encoding_provider, request_fixture, ConnectionFixture, RequestFixture},
fixtures::{ConnectionFixture, encoding_provider},
hash::Blake3,
transcript::Transcript,
};
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
use crate::fixtures::{RequestFixture, request_fixture};
use super::*;
@@ -403,7 +402,10 @@ mod test {
let HandshakeData::V1_2(HandshakeDataV1_2 {
server_ephemeral_key,
..
}) = server_cert_data.handshake;
}) = server_cert_data.handshake
else {
panic!("expected v1.2 handshake data");
};
attestation_builder.server_ephemeral_key(server_ephemeral_key);
@@ -471,7 +473,10 @@ mod test {
let HandshakeData::V1_2(HandshakeDataV1_2 {
server_ephemeral_key,
..
}) = server_cert_data.handshake;
}) = server_cert_data.handshake
else {
panic!("expected v1.2 handshake data");
};
attestation_builder
.connection_info(connection_info)

View File

@@ -1,9 +1,9 @@
use std::{fmt::Debug, sync::Arc};
use tlsn_core::hash::HashAlgId;
use crate::{
attestation::{Extension, InvalidExtension},
hash::{HashAlgId, DEFAULT_SUPPORTED_HASH_ALGS},
signing::SignatureAlgId,
Extension, InvalidExtension, hash::DEFAULT_SUPPORTED_HASH_ALGS, signing::SignatureAlgId,
};
type ExtensionValidator = Arc<dyn Fn(&[Extension]) -> Result<(), InvalidExtension> + Send + Sync>;
@@ -124,7 +124,7 @@ impl AttestationConfigBuilder {
///
/// # Example
/// ```
/// # use tlsn_core::attestation::{AttestationConfig, InvalidExtension};
/// # use tlsn_attestation::{AttestationConfig, InvalidExtension};
/// # let mut builder = AttestationConfig::builder();
/// builder.extension_validator(|extensions| {
/// for extension in extensions {

View File

@@ -1,16 +1,65 @@
//! Types for proving details of a connection.
//! Types for committing details of a connection.
//!
//! ## Commitment
//!
//! During the TLS handshake the Notary receives the Server's ephemeral public
//! key, and this key serves as a binding commitment to the identity of the
//! Server. The ephemeral key itself does not reveal the Server's identity, but
//! it is bound to it via a signature created using the Server's
//! X.509 certificate.
//!
//! A Prover can withhold the Server's signature and certificate chain from the
//! Notary to improve privacy and censorship resistance.
//!
//! ## Proving the Server's identity
//!
//! A Prover can prove the Server's identity to a Verifier by sending a
//! [`ServerIdentityProof`]. This proof contains all the information required to
//! establish the link between the TLS connection and the Server's X.509
//! certificate. A Verifier checks the Server's certificate against their own
//! trust anchors, the same way a typical TLS client would.
use serde::{Deserialize, Serialize};
use crate::{
connection::{
commit::{ServerCertCommitment, ServerCertOpening},
CertificateVerificationError, ServerEphemKey, ServerName,
},
hash::{HashAlgorithmExt, HashProviderError},
CryptoProvider,
use tlsn_core::{
connection::{CertificateVerificationError, ServerCertData, ServerEphemKey, ServerName},
hash::{Blinded, HashAlgorithm, HashProviderError, TypedHash},
};
use crate::{CryptoProvider, hash::HashAlgorithmExt, serialize::impl_domain_separator};
/// Opens a [`ServerCertCommitment`].
#[derive(Clone, Serialize, Deserialize)]
pub struct ServerCertOpening(Blinded<ServerCertData>);
impl_domain_separator!(ServerCertOpening);
opaque_debug::implement!(ServerCertOpening);
impl ServerCertOpening {
pub(crate) fn new(data: ServerCertData) -> Self {
Self(Blinded::new(data))
}
pub(crate) fn commit(&self, hasher: &dyn HashAlgorithm) -> ServerCertCommitment {
ServerCertCommitment(TypedHash {
alg: hasher.id(),
value: hasher.hash_separated(self),
})
}
/// Returns the server identity data.
pub fn data(&self) -> &ServerCertData {
self.0.data()
}
}
/// Commitment to a server certificate.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServerCertCommitment(pub(crate) TypedHash);
impl_domain_separator!(ServerCertCommitment);
/// TLS server identity proof.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerIdentityProof {
@@ -27,7 +76,7 @@ impl ServerIdentityProof {
///
/// # Arguments
///
/// * `provider` - The crypto provider to use for verification.
/// * `provider` - Crypto provider.
/// * `time` - The time of the connection.
/// * `server_ephemeral_key` - The server's ephemeral key.
/// * `commitment` - Commitment to the server certificate.
@@ -48,12 +97,9 @@ impl ServerIdentityProof {
}
// Verify certificate and identity.
self.opening.data().verify_with_provider(
provider,
time,
server_ephemeral_key,
&self.name,
)?;
self.opening
.data()
.verify(&provider.cert, time, server_ephemeral_key, &self.name)?;
Ok(self.name)
}

View File

@@ -2,7 +2,7 @@ use std::error::Error;
use serde::{Deserialize, Serialize};
use crate::hash::impl_domain_separator;
use crate::serialize::impl_domain_separator;
/// An attestation extension.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]

View File

@@ -0,0 +1,124 @@
//! Attestation fixtures.
use tlsn_core::{
connection::{HandshakeData, HandshakeDataV1_2},
fixtures::ConnectionFixture,
hash::HashAlgorithm,
transcript::{
Transcript, TranscriptCommitConfigBuilder, TranscriptCommitment,
encoding::{EncodingProvider, EncodingTree},
},
};
use crate::{
Attestation, AttestationConfig, CryptoProvider, Extension,
request::{Request, RequestConfig},
signing::SignatureAlgId,
};
/// A Request fixture used for testing.
#[allow(missing_docs)]
pub struct RequestFixture {
pub encoding_tree: EncodingTree,
pub request: Request,
}
/// Returns a request fixture for testing.
pub fn request_fixture(
transcript: Transcript,
encodings_provider: impl EncodingProvider,
connection: ConnectionFixture,
encoding_hasher: impl HashAlgorithm,
extensions: Vec<Extension>,
) -> RequestFixture {
let provider = CryptoProvider::default();
let (sent_len, recv_len) = transcript.len();
let ConnectionFixture {
server_name,
server_cert_data,
..
} = connection;
let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript);
transcript_commitment_builder
.commit_sent(&(0..sent_len))
.unwrap()
.commit_recv(&(0..recv_len))
.unwrap();
let transcripts_commitment_config = transcript_commitment_builder.build().unwrap();
// Prover constructs encoding tree.
let encoding_tree = EncodingTree::new(
&encoding_hasher,
transcripts_commitment_config.iter_encoding(),
&encodings_provider,
)
.unwrap();
let mut builder = RequestConfig::builder();
for extension in extensions {
builder.extension(extension);
}
let request_config = builder.build().unwrap();
let mut request_builder = Request::builder(&request_config);
request_builder
.server_name(server_name)
.server_cert_data(server_cert_data)
.transcript(transcript);
let (request, _) = request_builder.build(&provider).unwrap();
RequestFixture {
encoding_tree,
request,
}
}
/// Returns an attestation fixture for testing.
pub fn attestation_fixture(
request: Request,
connection: ConnectionFixture,
signature_alg: SignatureAlgId,
transcript_commitments: &[TranscriptCommitment],
) -> Attestation {
let ConnectionFixture {
connection_info,
server_cert_data,
..
} = connection;
let HandshakeData::V1_2(HandshakeDataV1_2 {
server_ephemeral_key,
..
}) = server_cert_data.handshake
else {
panic!("expected v1.2 handshake data");
};
let mut provider = CryptoProvider::default();
match signature_alg {
SignatureAlgId::SECP256K1 => provider.signer.set_secp256k1(&[42u8; 32]).unwrap(),
SignatureAlgId::SECP256R1 => provider.signer.set_secp256r1(&[42u8; 32]).unwrap(),
_ => unimplemented!(),
};
let attestation_config = AttestationConfig::builder()
.supported_signature_algs([signature_alg])
.build()
.unwrap();
let mut attestation_builder = Attestation::builder(&attestation_config)
.accept_request(request)
.unwrap();
attestation_builder
.connection_info(connection_info)
.server_ephemeral_key(server_ephemeral_key)
.transcript_commitments(transcript_commitments.to_vec());
attestation_builder.build(&provider).unwrap()
}

View File

@@ -0,0 +1,19 @@
use tlsn_core::hash::{Hash, HashAlgId, HashAlgorithm};
use crate::serialize::{CanonicalSerialize, DomainSeparator};
pub(crate) const DEFAULT_SUPPORTED_HASH_ALGS: &[HashAlgId] =
&[HashAlgId::SHA256, HashAlgId::BLAKE3, HashAlgId::KECCAK256];
pub(crate) trait HashAlgorithmExt: HashAlgorithm {
#[allow(dead_code)]
fn hash_canonical<T: CanonicalSerialize>(&self, data: &T) -> Hash {
self.hash(&data.serialize())
}
fn hash_separated<T: DomainSeparator + CanonicalSerialize>(&self, data: &T) -> Hash {
self.hash_prefixed(data.domain(), &data.serialize())
}
}
impl<T: HashAlgorithm + ?Sized> HashAlgorithmExt for T {}

View File

@@ -1,4 +1,28 @@
//! Attestation types.
//! TLSNotary attestation types.
//!
//! # Introduction
//!
//! This library provides core functionality for TLSNotary **attestations**.
//!
//! Once the TLS commitment protocol has been completed the Prover holds a
//! collection of commitments pertaining to the TLS connection. Most
//! importantly, the Prover is committed to the
//! [`ServerName`](tlsn_core::connection::ServerName),
//! and the [`Transcript`](tlsn_core::transcript::Transcript) of application
//! data. Subsequently, the Prover can request an [`Attestation`] from the
//! Notary who will include the commitments as well as any additional
//! information which may be useful to an attestation Verifier.
//!
//! Holding an attestation, the Prover can construct a
//! [`Presentation`](crate::presentation::Presentation) which facilitates
//! selectively disclosing various aspects of the TLS connection to a Verifier.
//! If the Verifier trusts the Notary, or more specifically the verifying key of
//! the attestation, then the Verifier can trust the authenticity of the
//! information disclosed in the presentation.
//!
//! **Be sure to check out the various submodules for more information.**
//!
//! # Structure
//!
//! An attestation is a cryptographically signed document issued by a Notary who
//! witnessed a TLS connection. It contains various fields which can be used to
@@ -24,37 +48,194 @@
//! extensions](crate::request::RequestConfigBuilder::extension)
//! to their attestation request, provided that the Notary supports them
//! (disallowed by default). A Notary may also be configured to
//! [validate](crate::attestation::AttestationConfigBuilder::extension_validator)
//! [validate](crate::AttestationConfigBuilder::extension_validator)
//! any extensions requested by a Prover using custom application logic.
//! Additionally, a Notary may
//! [include](crate::attestation::AttestationBuilder::extension)
//! [include](crate::AttestationBuilder::extension)
//! their own extensions.
//!
//! # Committing to the transcript
//!
//! The TLS commitment protocol produces commitments to the entire transcript of
//! application data. However, we may want to disclose only a subset of the data
//! in a presentation. Prior to attestation, the Prover has the opportunity to
//! slice and dice the commitments into smaller sections which can be
//! selectively disclosed. Additionally, the Prover may want to use different
//! commitment schemes depending on the context they expect to disclose.
//!
//! The primary API for this process is the
//! [`TranscriptCommitConfigBuilder`](tlsn_core::transcript::TranscriptCommitConfigBuilder)
//! which is used to build up a configuration.
//!
//! ```no_run
//! # use tlsn_core::transcript::{TranscriptCommitConfigBuilder, Transcript, Direction};
//! # use tlsn_core::hash::HashAlgId;
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let transcript: Transcript = unimplemented!();
//! let (sent_len, recv_len) = transcript.len();
//!
//! // Create a new configuration builder.
//! let mut builder = TranscriptCommitConfigBuilder::new(&transcript);
//!
//! // Specify all the transcript commitments we want to make.
//! builder
//! // Use BLAKE3 for encoding commitments.
//! .encoding_hash_alg(HashAlgId::BLAKE3)
//! // Commit to all sent data.
//! .commit_sent(&(0..sent_len))?
//! // Commit to the first 10 bytes of sent data.
//! .commit_sent(&(0..10))?
//! // Skip some bytes so it can be omitted in the presentation.
//! .commit_sent(&(20..sent_len))?
//! // Commit to all received data.
//! .commit_recv(&(0..recv_len))?;
//!
//! let config = builder.build()?;
//! # Ok(())
//! # }
//! ```
//!
//! # Requesting an attestation
//!
//! The first step in the attestation protocol is for the Prover to make a
//! [`Request`](crate::request::Request), which can be configured using the
//! associated [builder](crate::request::RequestConfigBuilder). With it the
//! Prover can configure some of the details of the attestation, such as which
//! cryptographic algorithms are used (if the Notary supports them).
//!
//! The Prover may also request for extensions to be added to the attestation,
//! see [here](#extensions) for more information.
//!
//! Upon being issued an attestation, the Prover will also hold a corresponding
//! [`Secrets`] which contains all private information. This pair can be stored
//! and used later to construct a
//! [`Presentation`](crate::presentation::Presentation), [see
//! below](#constructing-a-presentation).
//!
//! # Issuing an attestation
//!
//! Upon receiving a request, the Notary can issue an [`Attestation`] which can
//! be configured using the associated
//! [builder](crate::AttestationConfigBuilder).
//!
//! The Notary's [`CryptoProvider`] must be configured with an appropriate
//! signing key for attestations. See
//! [`SignerProvider`](crate::signing::SignerProvider) for more information.
//!
//! # Constructing a presentation
//!
//! A Prover can use an [`Attestation`] and the corresponding [`Secrets`] to
//! construct a verifiable [`Presentation`](crate::presentation::Presentation).
//!
//! ```no_run
//! # use tlsn_attestation::{Attestation, CryptoProvider, Secrets, presentation::Presentation};
//! # use tlsn_core::transcript::{TranscriptCommitmentKind, Direction};
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let attestation: Attestation = unimplemented!();
//! # let secrets: Secrets = unimplemented!();
//! # let crypto_provider: CryptoProvider = unimplemented!();
//! let (_sent_len, recv_len) = secrets.transcript().len();
//!
//! // First, we decide which application data we would like to disclose.
//! let mut builder = secrets.transcript_proof_builder();
//!
//! builder
//! // Use transcript encoding commitments.
//! .commitment_kinds(&[TranscriptCommitmentKind::Encoding])
//! // Disclose the first 10 bytes of the sent data.
//! .reveal(&(0..10), Direction::Sent)?
//! // Disclose all of the received data.
//! .reveal(&(0..recv_len), Direction::Received)?;
//!
//! let transcript_proof = builder.build()?;
//!
//! // Most cases we will also disclose the server identity.
//! let identity_proof = secrets.identity_proof();
//!
//! // Now we can construct the presentation.
//! let mut builder = attestation.presentation_builder(&crypto_provider);
//!
//! builder
//! .identity_proof(identity_proof)
//! .transcript_proof(transcript_proof);
//!
//! // Finally, we build the presentation. Send it to a verifier!
//! let presentation: Presentation = builder.build()?;
//! # Ok(())
//! # }
//! ```
//!
//! # Verifying a presentation
//!
//! Verifying a presentation is as simple as checking the verifier trusts the
//! verifying key then calling
//! [`Presentation::verify`](crate::presentation::Presentation::verify).
//!
//! ```no_run
//! # use tlsn_attestation::{CryptoProvider, presentation::{Presentation, PresentationOutput}, signing::VerifyingKey};
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let presentation: Presentation = unimplemented!();
//! # let trusted_key: VerifyingKey = unimplemented!();
//! # let crypto_provider: CryptoProvider = unimplemented!();
//! // Assert that we trust the verifying key.
//! assert_eq!(presentation.verifying_key(), &trusted_key);
//!
//! let PresentationOutput {
//! attestation,
//! server_name,
//! connection_info,
//! transcript,
//! ..
//! } = presentation.verify(&crypto_provider)?;
//! # Ok(())
//! # }
//! ```
#![deny(missing_docs, unreachable_pub, unused_must_use)]
#![deny(clippy::all)]
#![forbid(unsafe_code)]
mod builder;
mod config;
pub mod connection;
mod extension;
#[cfg(any(test, feature = "fixtures"))]
pub mod fixtures;
pub(crate) mod hash;
pub mod presentation;
mod proof;
mod provider;
pub mod request;
mod secrets;
pub(crate) mod serialize;
pub mod signing;
use std::fmt;
use rand::distr::{Distribution, StandardUniform};
use serde::{Deserialize, Serialize};
use crate::{
connection::{ConnectionInfo, ServerCertCommitment, ServerEphemKey},
hash::{impl_domain_separator, Hash, HashAlgorithm, HashAlgorithmExt, TypedHash},
use tlsn_core::{
connection::{ConnectionInfo, ServerEphemKey},
hash::{Hash, HashAlgorithm, TypedHash},
merkle::MerkleTree,
presentation::PresentationBuilder,
signing::{Signature, VerifyingKey},
transcript::TranscriptCommitment,
CryptoProvider,
};
use crate::{
connection::ServerCertCommitment,
hash::HashAlgorithmExt,
presentation::PresentationBuilder,
serialize::impl_domain_separator,
signing::{Signature, VerifyingKey},
};
pub use builder::{AttestationBuilder, AttestationBuilderError};
pub use config::{AttestationConfig, AttestationConfigBuilder, AttestationConfigError};
pub use extension::{Extension, InvalidExtension};
pub use proof::{AttestationError, AttestationProof};
pub use provider::CryptoProvider;
pub use secrets::Secrets;
/// Current version of attestations.
pub const VERSION: Version = Version(0);
@@ -127,8 +308,6 @@ pub enum FieldKind {
}
/// Attestation header.
///
/// See [module level documentation](crate::attestation) for more information.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Header {
/// An identifier for the attestation.
@@ -142,8 +321,6 @@ pub struct Header {
impl_domain_separator!(Header);
/// Attestation body.
///
/// See [module level documentation](crate::attestation) for more information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Body {
verifying_key: Field<VerifyingKey>,
@@ -246,8 +423,6 @@ impl Body {
}
/// An attestation document.
///
/// See [module level documentation](crate::attestation) for more information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attestation {
/// The signature of the attestation.

View File

@@ -26,12 +26,15 @@ use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{
attestation::{Attestation, AttestationError, AttestationProof, Extension},
connection::{ConnectionInfo, ServerIdentityProof, ServerIdentityProofError, ServerName},
signing::VerifyingKey,
use tlsn_core::{
connection::{ConnectionInfo, ServerName},
transcript::{PartialTranscript, TranscriptProof, TranscriptProofError},
CryptoProvider,
};
use crate::{
Attestation, AttestationError, AttestationProof, CryptoProvider, Extension,
connection::{ServerIdentityProof, ServerIdentityProofError},
signing::VerifyingKey,
};
/// A verifiable presentation.
@@ -86,7 +89,7 @@ impl Presentation {
let transcript = transcript
.map(|transcript| {
transcript.verify_with_provider(
provider,
&provider.hash,
&attestation.body.connection_info().transcript_length,
attestation.body.transcript_commitments(),
)
@@ -186,7 +189,7 @@ impl fmt::Display for PresentationBuilderError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())
@@ -227,7 +230,7 @@ impl fmt::Display for PresentationError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())

View File

@@ -2,13 +2,15 @@ use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{
attestation::{Attestation, Body, Header},
use tlsn_core::{
hash::HashAlgorithm,
merkle::{MerkleProof, MerkleTree},
};
use crate::{
Attestation, Body, CryptoProvider, Header,
serialize::CanonicalSerialize,
signing::{Signature, VerifyingKey},
CryptoProvider,
};
/// Proof of an attestation.
@@ -165,7 +167,7 @@ impl fmt::Display for AttestationError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())

View File

@@ -2,11 +2,9 @@ use tls_core::{
anchors::{OwnedTrustAnchor, RootCertStore},
verify::WebPkiVerifier,
};
use tlsn_core::hash::HashProvider;
use crate::{
hash::HashProvider,
signing::{SignatureVerifierProvider, SignerProvider},
};
use crate::signing::{SignatureVerifierProvider, SignerProvider};
/// Cryptography provider.
///
@@ -17,7 +15,7 @@ use crate::{
/// implementations.
///
/// Algorithms are uniquely identified using an 8-bit ID, eg.
/// [`HashAlgId`](crate::hash::HashAlgId), half of which is reserved for the
/// [`HashAlgId`](tlsn_core::hash::HashAlgId), half of which is reserved for the
/// officially supported algorithms. If you think that a new algorithm should be
/// added to the official set, please open an issue. Beware that other parties
/// may assign different algorithms to the same ID as you, and we make no effort

View File

@@ -18,12 +18,9 @@ mod config;
use serde::{Deserialize, Serialize};
use crate::{
attestation::{Attestation, Extension},
connection::ServerCertCommitment,
hash::HashAlgId,
signing::SignatureAlgId,
};
use tlsn_core::hash::HashAlgId;
use crate::{Attestation, Extension, connection::ServerCertCommitment, signing::SignatureAlgId};
pub use builder::{RequestBuilder, RequestBuilderError};
pub use config::{RequestConfig, RequestConfigBuilder, RequestConfigBuilderError};
@@ -85,18 +82,19 @@ pub struct InconsistentAttestation(String);
#[cfg(test)]
mod test {
use tlsn_core::{
connection::TranscriptLength,
fixtures::{ConnectionFixture, encoding_provider},
hash::{Blake3, HashAlgId},
transcript::Transcript,
};
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
use crate::{
connection::{ServerCertOpening, TranscriptLength},
fixtures::{
attestation_fixture, encoding_provider, request_fixture, ConnectionFixture,
RequestFixture,
},
hash::{Blake3, HashAlgId},
signing::SignatureAlgId,
transcript::Transcript,
CryptoProvider,
connection::ServerCertOpening,
fixtures::{RequestFixture, attestation_fixture, request_fixture},
signing::SignatureAlgId,
};
#[test]

View File

@@ -1,9 +1,12 @@
use crate::{
connection::{ServerCertData, ServerCertOpening, ServerName},
request::{Request, RequestConfig},
secrets::Secrets,
use tlsn_core::{
connection::{ServerCertData, ServerName},
transcript::{Transcript, TranscriptCommitment, TranscriptSecret},
CryptoProvider,
};
use crate::{
CryptoProvider, Secrets,
connection::ServerCertOpening,
request::{Request, RequestConfig},
};
/// Builder for [`Request`].

View File

@@ -1,7 +1,6 @@
use crate::{
attestation::Extension, hash::HashAlgId, signing::SignatureAlgId,
transcript::TranscriptCommitConfig,
};
use tlsn_core::{hash::HashAlgId, transcript::TranscriptCommitConfig};
use crate::{Extension, signing::SignatureAlgId};
/// Request configuration.
#[derive(Debug, Clone)]

View File

@@ -1,11 +1,13 @@
use serde::{Deserialize, Serialize};
use crate::{
connection::{ServerCertOpening, ServerIdentityProof, ServerName},
use tlsn_core::{
connection::ServerName,
transcript::{Transcript, TranscriptCommitment, TranscriptProofBuilder, TranscriptSecret},
};
/// Secret data of an [`Attestation`](crate::attestation::Attestation).
use crate::connection::{ServerCertOpening, ServerIdentityProof};
/// Secret data of an [`Attestation`](crate::Attestation).
#[derive(Clone, Serialize, Deserialize)]
pub struct Secrets {
pub(crate) server_name: ServerName,

View File

@@ -0,0 +1,53 @@
/// Canonical serialization of TLSNotary types.
///
/// This trait is used to serialize types into a canonical byte representation.
pub(crate) trait CanonicalSerialize {
/// Serializes the type.
fn serialize(&self) -> Vec<u8>;
}
impl<T> CanonicalSerialize for T
where
T: serde::Serialize,
{
fn serialize(&self) -> Vec<u8> {
// For now we use BCS for serialization. In future releases we will want to
// consider this further, particularly with respect to EVM compatibility.
bcs::to_bytes(self).unwrap()
}
}
/// A type with a domain separator which is used during hashing to mitigate type
/// confusion attacks.
pub(crate) trait DomainSeparator {
/// Returns the domain separator for the type.
fn domain(&self) -> &[u8];
}
macro_rules! impl_domain_separator {
($type:ty) => {
impl $crate::serialize::DomainSeparator for $type {
fn domain(&self) -> &[u8] {
use std::sync::LazyLock;
// Computes a 16 byte hash of the type's name to use as a domain separator.
static DOMAIN: LazyLock<[u8; 16]> = LazyLock::new(|| {
let domain: [u8; 32] = blake3::hash(stringify!($type).as_bytes()).into();
domain[..16].try_into().unwrap()
});
&*DOMAIN
}
}
};
}
pub(crate) use impl_domain_separator;
impl_domain_separator!(tlsn_core::connection::ServerEphemKey);
impl_domain_separator!(tlsn_core::connection::ConnectionInfo);
impl_domain_separator!(tlsn_core::connection::HandshakeData);
impl_domain_separator!(tlsn_core::transcript::TranscriptCommitment);
impl_domain_separator!(tlsn_core::transcript::TranscriptSecret);
impl_domain_separator!(tlsn_core::transcript::encoding::EncodingCommitment);
impl_domain_separator!(tlsn_core::transcript::hash::PlaintextHash);

View File

@@ -4,7 +4,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::hash::impl_domain_separator;
use crate::serialize::impl_domain_separator;
/// Key algorithm identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -242,8 +242,8 @@ mod secp256k1 {
use std::sync::{Arc, Mutex};
use k256::ecdsa::{
signature::{SignerMut, Verifier},
Signature as Secp256K1Signature, SigningKey,
signature::{SignerMut, Verifier},
};
use super::*;
@@ -318,8 +318,8 @@ mod secp256r1 {
use std::sync::{Arc, Mutex};
use p256::ecdsa::{
signature::{SignerMut, Verifier},
Signature as Secp256R1Signature, SigningKey,
signature::{SignerMut, Verifier},
};
use super::*;
@@ -394,7 +394,7 @@ mod secp256k1eth {
use std::sync::{Arc, Mutex};
use k256::ecdsa::{
signature::hazmat::PrehashVerifier, Signature as Secp256K1Signature, SigningKey,
Signature as Secp256K1Signature, SigningKey, signature::hazmat::PrehashVerifier,
};
use tiny_keccak::{Hasher, Keccak};
@@ -437,8 +437,7 @@ mod secp256k1eth {
// Based on Ethereum Yellow Paper Appendix F, only values 0 and 1 are valid.
if recid > 1 {
return Err(SignatureError(format!(
"expected recovery id 0 or 1, got {:?}",
recid
"expected recovery id 0 or 1, got {recid:?}"
)));
}
// `ecrecover` expects that 0 and 1 are mapped to 27 and 28.

View File

@@ -1,17 +1,18 @@
use tlsn_core::{
attestation::{Attestation, AttestationConfig},
connection::{HandshakeData, HandshakeDataV1_2},
fixtures::{self, encoder_secret, ConnectionFixture},
hash::Blake3,
use tlsn_attestation::{
Attestation, AttestationConfig, CryptoProvider,
presentation::PresentationOutput,
request::{Request, RequestConfig},
signing::SignatureAlgId,
};
use tlsn_core::{
connection::{HandshakeData, HandshakeDataV1_2},
fixtures::{self, ConnectionFixture, encoder_secret},
hash::Blake3,
transcript::{
encoding::{EncodingCommitment, EncodingTree},
Direction, Transcript, TranscriptCommitConfigBuilder, TranscriptCommitment,
TranscriptSecret,
encoding::{EncodingCommitment, EncodingTree},
},
CryptoProvider,
};
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};

View File

@@ -1,44 +0,0 @@
[package]
name = "tlsn-common"
description = "Common code shared between tlsn-prover and tlsn-verifier"
version = "0.1.0-alpha.12"
edition = "2021"
[lints]
workspace = true
[features]
default = []
[dependencies]
tlsn-core = { workspace = true }
tlsn-tls-core = { workspace = true }
tlsn-cipher = { workspace = true }
mpz-core = { workspace = true }
mpz-common = { workspace = true }
mpz-memory-core = { workspace = true }
mpz-hash = { workspace = true }
mpz-vm-core = { workspace = true }
mpz-zk = { workspace = true }
async-trait = { workspace = true }
derive_builder = { workspace = true }
futures = { workspace = true }
ghash = { workspace = true }
once_cell = { workspace = true }
opaque-debug = { workspace = true }
rand = { workspace = true }
rangeset = { workspace = true }
serio = { workspace = true, features = ["codec", "bincode"] }
thiserror = { workspace = true }
tracing = { workspace = true }
uid-mux = { workspace = true, features = ["serio"] }
serde = { workspace = true, features = ["derive"] }
semver = { version = "1.0", features = ["serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }
web-spawn = { workspace = true }
[dev-dependencies]
rstest = { workspace = true }

View File

@@ -1,27 +0,0 @@
//! Common code shared between `tlsn-prover` and `tlsn-verifier`.
#![deny(missing_docs, unreachable_pub, unused_must_use)]
#![deny(clippy::all)]
#![forbid(unsafe_code)]
pub mod commit;
pub mod config;
pub mod context;
pub mod encoding;
pub mod ghash;
pub mod msg;
pub mod mux;
pub mod tag;
pub mod transcript;
pub mod zk_aes_ctr;
/// The party's role in the TLSN protocol.
///
/// A Notary is classified as a Verifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
/// The prover.
Prover,
/// The verifier.
Verifier,
}

View File

@@ -5,7 +5,7 @@ description = "This crate provides implementations of ciphers for two parties"
keywords = ["tls", "mpc", "2pc", "aes"]
categories = ["cryptography"]
license = "MIT OR Apache-2.0"
version = "0.1.0-alpha.12"
version = "0.1.0-alpha.13-pre"
edition = "2021"
[lints]

View File

@@ -36,7 +36,7 @@ impl Display for AesError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())

View File

@@ -1,6 +1,6 @@
[package]
name = "tlsn-deap"
version = "0.1.0-alpha.12"
version = "0.1.0-alpha.13-pre"
edition = "2021"
[lints]

View File

@@ -5,7 +5,7 @@ description = "A 2PC implementation of TLS HMAC-SHA256 PRF"
keywords = ["tls", "mpc", "2pc", "hmac", "sha256"]
categories = ["cryptography"]
license = "MIT OR Apache-2.0"
version = "0.1.0-alpha.12"
version = "0.1.0-alpha.13-pre"
edition = "2021"
[lints]

View File

@@ -56,7 +56,7 @@ impl fmt::Display for PrfError {
}
if let Some(ref source) = self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())

View File

@@ -5,7 +5,7 @@ description = "Implementation of the 3-party key-exchange protocol"
keywords = ["tls", "mpc", "2pc", "pms", "key-exchange"]
categories = ["cryptography"]
license = "MIT OR Apache-2.0"
version = "0.1.0-alpha.12"
version = "0.1.0-alpha.13-pre"
edition = "2021"
[lints]

View File

@@ -5,7 +5,7 @@ description = "Core types for TLSNotary"
keywords = ["tls", "mpc", "2pc", "types"]
categories = ["cryptography"]
license = "MIT OR Apache-2.0"
version = "0.1.0-alpha.12"
version = "0.1.0-alpha.13-pre"
edition = "2021"
[lints]
@@ -21,13 +21,10 @@ tlsn-tls-core = { workspace = true, features = ["serde"] }
tlsn-utils = { workspace = true }
rangeset = { workspace = true, features = ["serde"] }
bcs = { workspace = true }
bimap = { version = "0.6", features = ["serde"] }
blake3 = { workspace = true }
hex = { workspace = true, optional = true }
k256 = { workspace = true }
opaque-debug = { workspace = true }
p256 = { workspace = true, features = ["serde"] }
rand = { workspace = true }
rand_core = { workspace = true }
rand_chacha = { workspace = true }
@@ -36,21 +33,13 @@ rstest = { workspace = true, optional = true }
serde = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tiny-keccak = { version = "2.0", features = ["keccak"] }
tiny-keccak = { workspace = true, features = ["keccak"] }
web-time = { workspace = true }
webpki-roots = { workspace = true }
itybity = { workspace = true }
[dev-dependencies]
alloy-primitives = { version = "0.8.22", default-features = false }
alloy-signer = { version = "0.12", default-features = false }
alloy-signer-local = { version = "0.12", default-features = false }
bincode = { workspace = true }
hex = { workspace = true }
rstest = { workspace = true }
tlsn-data-fixtures = { workspace = true }
rand06-compat = { workspace = true }
[[test]]
name = "api"
required-features = ["fixtures"]

View File

@@ -1,26 +1,4 @@
//! TLS connection types.
//!
//! ## Commitment
//!
//! During the TLS handshake the Notary receives the Server's ephemeral public
//! key, and this key serves as a binding commitment to the identity of the
//! Server. The ephemeral key itself does not reveal the Server's identity, but
//! it is bound to it via a signature created using the Server's
//! X.509 certificate.
//!
//! A Prover can withhold the Server's signature and certificate chain from the
//! Notary to improve privacy and censorship resistance.
//!
//! ## Proving the Server's identity
//!
//! A Prover can prove the Server's identity to a Verifier by sending a
//! [`ServerIdentityProof`]. This proof contains all the information required to
//! establish the link between the TLS connection and the Server's X.509
//! certificate. A Verifier checks the Server's certificate against their own
//! trust anchors, the same way a typical TLS client would.
mod commit;
mod proof;
use std::fmt;
@@ -31,15 +9,10 @@ use tls_core::{
enums::NamedGroup,
handshake::{DigitallySignedStruct, ServerECDHParams},
},
verify::ServerCertVerifier as _,
verify::{ServerCertVerifier as _, WebPkiVerifier},
};
use web_time::{Duration, UNIX_EPOCH};
use crate::{hash::impl_domain_separator, CryptoProvider};
pub use commit::{ServerCertCommitment, ServerCertOpening};
pub use proof::{ServerIdentityProof, ServerIdentityProofError};
/// TLS version.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -198,8 +171,6 @@ pub struct ServerEphemKey {
pub key: Vec<u8>,
}
impl_domain_separator!(ServerEphemKey);
impl ServerEphemKey {
/// Encodes the key exchange parameters as in TLS.
pub(crate) fn kx_params(&self) -> Vec<u8> {
@@ -240,8 +211,6 @@ pub struct ConnectionInfo {
pub transcript_length: TranscriptLength,
}
impl_domain_separator!(ConnectionInfo);
/// Transcript length information.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TranscriptLength {
@@ -271,7 +240,14 @@ pub enum HandshakeData {
V1_2(HandshakeDataV1_2),
}
impl_domain_separator!(HandshakeData);
/// Verify data from the TLS handshake finished messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyData {
/// Client finished verify data.
pub client_finished: Vec<u8>,
/// Server finished verify data.
pub server_finished: Vec<u8>,
}
/// Server certificate and handshake data.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -289,13 +265,13 @@ impl ServerCertData {
///
/// # Arguments
///
/// * `provider` - The crypto provider to use for verification.
/// * `verifier` - Cerificate verifier.
/// * `time` - The time of the connection.
/// * `server_ephemeral_key` - The server's ephemeral key.
/// * `server_name` - The server name.
pub fn verify_with_provider(
pub fn verify(
&self,
provider: &CryptoProvider,
verifier: &WebPkiVerifier,
time: u64,
server_ephemeral_key: &ServerEphemKey,
server_name: &ServerName,
@@ -332,8 +308,7 @@ impl ServerCertData {
// Verify the end entity cert is valid for the provided server name
// and that it chains to at least one of the roots we trust.
provider
.cert
verifier
.verify_server_cert(
end_entity,
intermediates,
@@ -352,8 +327,7 @@ impl ServerCertData {
let dss = DigitallySignedStruct::new(self.sig.scheme.into(), self.sig.sig.clone());
provider
.cert
verifier
.verify_tls12_signature(&message, end_entity, &dss)
.map_err(|_| CertificateVerificationError::InvalidServerSignature)?;
@@ -380,19 +354,27 @@ pub enum CertificateVerificationError {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
fixtures::ConnectionFixture, provider::default_cert_verifier, transcript::Transcript,
};
use crate::{fixtures::ConnectionFixture, transcript::Transcript};
use hex::FromHex;
use rstest::*;
use tls_core::verify::WebPkiVerifier;
use tls_core::{
anchors::{OwnedTrustAnchor, RootCertStore},
verify::WebPkiVerifier,
};
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
#[fixture]
#[once]
fn crypto_provider() -> CryptoProvider {
let mut store = default_cert_verifier().root_store().clone();
fn verifier() -> WebPkiVerifier {
let mut root_store = RootCertStore::empty();
root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject.as_ref(),
ta.subject_public_key_info.as_ref(),
ta.name_constraints.as_ref().map(|nc| nc.as_ref()),
)
}));
// Add a cert which is no longer included in the Mozilla root store.
let cert = tls_core::key::Certificate(
@@ -405,14 +387,9 @@ mod tests {
.clone(),
);
store.add(&cert).unwrap();
root_store.add(&cert).unwrap();
CryptoProvider {
hash: Default::default(),
cert: WebPkiVerifier::new(store, None),
signer: Default::default(),
signature: Default::default(),
}
WebPkiVerifier::new(root_store, None)
}
fn tlsnotary() -> ConnectionFixture {
@@ -428,7 +405,7 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_cert_chain_sucess_ca_implicit(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] mut data: ConnectionFixture,
) {
// Remove the CA cert
@@ -436,8 +413,8 @@ mod tests {
assert!(data
.server_cert_data
.verify_with_provider(
crypto_provider,
.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -451,13 +428,13 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_cert_chain_success_ca_explicit(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] data: ConnectionFixture,
) {
assert!(data
.server_cert_data
.verify_with_provider(
crypto_provider,
.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -470,14 +447,14 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_cert_chain_fail_bad_time(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] data: ConnectionFixture,
) {
// unix time when the cert chain was NOT valid
let bad_time: u64 = 1571465711;
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
bad_time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -494,7 +471,7 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_cert_chain_fail_no_interm_cert(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] mut data: ConnectionFixture,
) {
// Remove the CA cert
@@ -502,8 +479,8 @@ mod tests {
// Remove the intermediate cert
data.server_cert_data.certs.pop();
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -521,14 +498,14 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_cert_chain_fail_no_interm_cert_with_ca_cert(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] mut data: ConnectionFixture,
) {
// Remove the intermediate cert
data.server_cert_data.certs.remove(1);
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -545,7 +522,7 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_cert_chain_fail_bad_ee_cert(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] mut data: ConnectionFixture,
) {
let ee: &[u8] = include_bytes!("./fixtures/data/unknown/ee.der");
@@ -553,8 +530,8 @@ mod tests {
// Change the end entity cert
data.server_cert_data.certs[0] = Certificate(ee.to_vec());
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -571,15 +548,15 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_sig_ke_params_fail_bad_client_random(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] mut data: ConnectionFixture,
) {
let HandshakeData::V1_2(HandshakeDataV1_2 { client_random, .. }) =
&mut data.server_cert_data.handshake;
client_random[31] = client_random[31].wrapping_add(1);
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -596,13 +573,13 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_sig_ke_params_fail_bad_sig(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] mut data: ConnectionFixture,
) {
data.server_cert_data.sig.sig[31] = data.server_cert_data.sig.sig[31].wrapping_add(1);
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),
@@ -619,13 +596,13 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_check_dns_name_present_in_cert_fail_bad_host(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] data: ConnectionFixture,
) {
let bad_name = ServerName::from("badhost.com");
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&bad_name,
@@ -641,17 +618,14 @@ mod tests {
#[rstest]
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_invalid_ephemeral_key(
crypto_provider: &CryptoProvider,
#[case] data: ConnectionFixture,
) {
fn test_invalid_ephemeral_key(verifier: &WebPkiVerifier, #[case] data: ConnectionFixture) {
let wrong_ephemeral_key = ServerEphemKey {
typ: KeyType::SECP256R1,
key: Vec::<u8>::from_hex(include_bytes!("./fixtures/data/unknown/pubkey")).unwrap(),
};
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
&wrong_ephemeral_key,
&ServerName::from(data.server_name.as_ref()),
@@ -668,14 +642,14 @@ mod tests {
#[case::tlsnotary(tlsnotary())]
#[case::appliedzkp(appliedzkp())]
fn test_verify_cert_chain_fail_no_cert(
crypto_provider: &CryptoProvider,
verifier: &WebPkiVerifier,
#[case] mut data: ConnectionFixture,
) {
// Empty certs
data.server_cert_data.certs = Vec::new();
let err = data.server_cert_data.verify_with_provider(
crypto_provider,
let err = data.server_cert_data.verify(
verifier,
data.connection_info.time,
data.server_ephemeral_key(),
&ServerName::from(data.server_name.as_ref()),

View File

@@ -1,40 +0,0 @@
//! Types for committing details of a connection.
use serde::{Deserialize, Serialize};
use crate::{
connection::ServerCertData,
hash::{impl_domain_separator, Blinded, HashAlgorithm, HashAlgorithmExt, TypedHash},
};
/// Opens a [`ServerCertCommitment`].
#[derive(Clone, Serialize, Deserialize)]
pub struct ServerCertOpening(Blinded<ServerCertData>);
impl_domain_separator!(ServerCertOpening);
opaque_debug::implement!(ServerCertOpening);
impl ServerCertOpening {
pub(crate) fn new(data: ServerCertData) -> Self {
Self(Blinded::new(data))
}
pub(crate) fn commit(&self, hasher: &dyn HashAlgorithm) -> ServerCertCommitment {
ServerCertCommitment(TypedHash {
alg: hasher.id(),
value: hasher.hash_separated(self),
})
}
/// Returns the server identity data.
pub fn data(&self) -> &ServerCertData {
self.0.data()
}
}
/// Commitment to a server certificate.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServerCertCommitment(pub(crate) TypedHash);
impl_domain_separator!(ServerCertCommitment);

View File

@@ -5,22 +5,16 @@ mod provider;
pub use provider::FixtureEncodingProvider;
use hex::FromHex;
use p256::ecdsa::SigningKey;
use crate::{
attestation::{Attestation, AttestationConfig, Extension},
connection::{
Certificate, ConnectionInfo, HandshakeData, HandshakeDataV1_2, KeyType, ServerCertData,
ServerEphemKey, ServerName, ServerSignature, SignatureScheme, TlsVersion, TranscriptLength,
},
hash::HashAlgorithm,
request::{Request, RequestConfig},
signing::SignatureAlgId,
transcript::{
encoding::{EncoderSecret, EncodingProvider, EncodingTree},
Transcript, TranscriptCommitConfigBuilder, TranscriptCommitment,
encoding::{EncoderSecret, EncodingProvider},
Transcript,
},
CryptoProvider,
};
/// A fixture containing various TLS connection data.
@@ -152,112 +146,3 @@ pub fn encoder_secret_tampered_seed() -> EncoderSecret {
seed[0] += 1;
EncoderSecret::new(seed, DELTA)
}
/// Returns a notary signing key fixture.
pub fn notary_signing_key() -> SigningKey {
SigningKey::from_slice(&[1; 32]).unwrap()
}
/// A Request fixture used for testing.
#[allow(missing_docs)]
pub struct RequestFixture {
pub encoding_tree: EncodingTree,
pub request: Request,
}
/// Returns a request fixture for testing.
pub fn request_fixture(
transcript: Transcript,
encodings_provider: impl EncodingProvider,
connection: ConnectionFixture,
encoding_hasher: impl HashAlgorithm,
extensions: Vec<Extension>,
) -> RequestFixture {
let provider = CryptoProvider::default();
let (sent_len, recv_len) = transcript.len();
let ConnectionFixture {
server_name,
server_cert_data,
..
} = connection;
let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript);
transcript_commitment_builder
.commit_sent(&(0..sent_len))
.unwrap()
.commit_recv(&(0..recv_len))
.unwrap();
let transcripts_commitment_config = transcript_commitment_builder.build().unwrap();
// Prover constructs encoding tree.
let encoding_tree = EncodingTree::new(
&encoding_hasher,
transcripts_commitment_config.iter_encoding(),
&encodings_provider,
)
.unwrap();
let mut builder = RequestConfig::builder();
for extension in extensions {
builder.extension(extension);
}
let request_config = builder.build().unwrap();
let mut request_builder = Request::builder(&request_config);
request_builder
.server_name(server_name)
.server_cert_data(server_cert_data)
.transcript(transcript);
let (request, _) = request_builder.build(&provider).unwrap();
RequestFixture {
encoding_tree,
request,
}
}
/// Returns an attestation fixture for testing.
pub fn attestation_fixture(
request: Request,
connection: ConnectionFixture,
signature_alg: SignatureAlgId,
transcript_commitments: &[TranscriptCommitment],
) -> Attestation {
let ConnectionFixture {
connection_info,
server_cert_data,
..
} = connection;
let HandshakeData::V1_2(HandshakeDataV1_2 {
server_ephemeral_key,
..
}) = server_cert_data.handshake;
let mut provider = CryptoProvider::default();
match signature_alg {
SignatureAlgId::SECP256K1 => provider.signer.set_secp256k1(&[42u8; 32]).unwrap(),
SignatureAlgId::SECP256R1 => provider.signer.set_secp256r1(&[42u8; 32]).unwrap(),
_ => unimplemented!(),
};
let attestation_config = AttestationConfig::builder()
.supported_signature_algs([signature_alg])
.build()
.unwrap();
let mut attestation_builder = Attestation::builder(&attestation_config)
.accept_request(request)
.unwrap();
attestation_builder
.connection_info(connection_info)
.server_ephemeral_key(server_ephemeral_key)
.transcript_commitments(transcript_commitments.to_vec());
attestation_builder.build(&provider).unwrap()
}

View File

@@ -5,11 +5,6 @@ use std::{collections::HashMap, fmt::Display};
use rand::{distr::StandardUniform, prelude::Distribution};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::serialize::CanonicalSerialize;
pub(crate) const DEFAULT_SUPPORTED_HASH_ALGS: &[HashAlgId] =
&[HashAlgId::SHA256, HashAlgId::BLAKE3, HashAlgId::KECCAK256];
/// Maximum length of a hash value.
const MAX_LEN: usize = 64;
@@ -238,19 +233,6 @@ pub trait HashAlgorithm {
fn hash_prefixed(&self, prefix: &[u8], data: &[u8]) -> Hash;
}
pub(crate) trait HashAlgorithmExt: HashAlgorithm {
#[allow(dead_code)]
fn hash_canonical<T: CanonicalSerialize>(&self, data: &T) -> Hash {
self.hash(&data.serialize())
}
fn hash_separated<T: DomainSeparator + CanonicalSerialize>(&self, data: &T) -> Hash {
self.hash_prefixed(data.domain(), &data.serialize())
}
}
impl<T: HashAlgorithm + ?Sized> HashAlgorithmExt for T {}
/// A hash blinder.
#[derive(Clone, Serialize, Deserialize)]
pub struct Blinder([u8; 16]);
@@ -274,52 +256,26 @@ impl Distribution<Blinder> for StandardUniform {
/// A blinded pre-image of a hash.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Blinded<T> {
pub struct Blinded<T> {
data: T,
blinder: Blinder,
}
impl<T> Blinded<T> {
/// Creates a new blinded pre-image.
pub(crate) fn new(data: T) -> Self {
pub fn new(data: T) -> Self {
Self {
data,
blinder: rand::random(),
}
}
pub(crate) fn data(&self) -> &T {
/// Returns the data.
pub fn data(&self) -> &T {
&self.data
}
}
/// A type with a domain separator which is used during hashing to mitigate type
/// confusion attacks.
pub(crate) trait DomainSeparator {
/// Returns the domain separator for the type.
fn domain(&self) -> &[u8];
}
macro_rules! impl_domain_separator {
($type:ty) => {
impl $crate::hash::DomainSeparator for $type {
fn domain(&self) -> &[u8] {
use std::sync::LazyLock;
// Computes a 16 byte hash of the type's name to use as a domain separator.
static DOMAIN: LazyLock<[u8; 16]> = LazyLock::new(|| {
let domain: [u8; 32] = blake3::hash(stringify!($type).as_bytes()).into();
domain[..16].try_into().unwrap()
});
&*DOMAIN
}
}
};
}
pub(crate) use impl_domain_separator;
mod sha2 {
use ::sha2::Digest;

View File

@@ -1,198 +1,16 @@
//! TLSNotary core library.
//!
//! # Introduction
//!
//! This library provides core functionality for the TLSNotary **attestation**
//! protocol, including some more general types which are useful outside
//! of attestations.
//!
//! Once the MPC-TLS protocol has been completed the Prover holds a collection
//! of commitments pertaining to the TLS connection. Most importantly, the
//! Prover is committed to the [`ServerName`](crate::connection::ServerName),
//! and the [`Transcript`](crate::transcript::Transcript) of application data.
//! Subsequently, the Prover can request an
//! [`Attestation`](crate::attestation::Attestation) from the Notary who will
//! include the commitments as well as any additional information which may be
//! useful to an attestation Verifier.
//!
//! Holding an attestation, the Prover can construct a
//! [`Presentation`](crate::presentation::Presentation) which facilitates
//! selectively disclosing various aspects of the TLS connection to a Verifier.
//! If the Verifier trusts the Notary, or more specifically the verifying key of
//! the attestation, then the Verifier can trust the authenticity of the
//! information disclosed in the presentation.
//!
//! **Be sure to check out the various submodules for more information.**
//!
//! # Committing to the transcript
//!
//! The MPC-TLS protocol produces commitments to the entire transcript of
//! application data. However, we may want to disclose only a subset of the data
//! in a presentation. Prior to attestation, the Prover has the opportunity to
//! slice and dice the commitments into smaller sections which can be
//! selectively disclosed. Additionally, the Prover may want to use different
//! commitment schemes depending on the context they expect to disclose.
//!
//! The primary API for this process is the
//! [`TranscriptCommitConfigBuilder`](crate::transcript::TranscriptCommitConfigBuilder)
//! which is used to build up a configuration.
//!
//! Currently, only the
//! [`Encoding`](crate::transcript::TranscriptCommitmentKind::Encoding)
//! commitment kind is supported. In the future you will be able to acquire hash
//! commitments directly to the transcript data.
//!
//! ```no_run
//! # use tlsn_core::transcript::{TranscriptCommitConfigBuilder, Transcript, Direction};
//! # use tlsn_core::hash::HashAlgId;
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let transcript: Transcript = unimplemented!();
//! let (sent_len, recv_len) = transcript.len();
//!
//! // Create a new configuration builder.
//! let mut builder = TranscriptCommitConfigBuilder::new(&transcript);
//!
//! // Specify all the transcript commitments we want to make.
//! builder
//! // Use BLAKE3 for encoding commitments.
//! .encoding_hash_alg(HashAlgId::BLAKE3)
//! // Commit to all sent data.
//! .commit_sent(&(0..sent_len))?
//! // Commit to the first 10 bytes of sent data.
//! .commit_sent(&(0..10))?
//! // Skip some bytes so it can be omitted in the presentation.
//! .commit_sent(&(20..sent_len))?
//! // Commit to all received data.
//! .commit_recv(&(0..recv_len))?;
//!
//! let config = builder.build()?;
//! # Ok(())
//! # }
//! ```
//!
//! # Requesting an attestation
//!
//! The first step in the attestation protocol is for the Prover to make a
//! [`Request`](crate::request::Request), which can be configured using the
//! associated [builder](crate::request::RequestConfigBuilder). With it the
//! Prover can configure some of the details of the attestation, such as which
//! cryptographic algorithms are used (if the Notary supports them).
//!
//! The Prover may also request for extensions to be added to the attestation,
//! see [here](crate::attestation#extensions) for more information.
//!
//! Upon being issued an attestation, the Prover will also hold a corresponding
//! [`Secrets`] which contains all private information. This pair can be stored
//! and used later to construct a
//! [`Presentation`](crate::presentation::Presentation), [see
//! below](#constructing-a-presentation).
//!
//! # Issuing an attestation
//!
//! Upon receiving a request, the Notary can issue an
//! [`Attestation`](crate::attestation::Attestation) which can be configured
//! using the associated
//! [builder](crate::attestation::AttestationConfigBuilder).
//!
//! The Notary's [`CryptoProvider`] must be configured with an appropriate
//! signing key for attestations. See
//! [`SignerProvider`](crate::signing::SignerProvider) for more information.
//!
//! # Constructing a presentation
//!
//! A Prover can use an [`Attestation`](crate::attestation::Attestation) and the
//! corresponding [`Secrets`] to construct a verifiable
//! [`Presentation`](crate::presentation::Presentation).
//!
//! ```no_run
//! # use tlsn_core::presentation::Presentation;
//! # use tlsn_core::attestation::Attestation;
//! # use tlsn_core::transcript::{TranscriptCommitmentKind, Direction};
//! # use tlsn_core::{Secrets, CryptoProvider};
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let attestation: Attestation = unimplemented!();
//! # let secrets: Secrets = unimplemented!();
//! # let crypto_provider: CryptoProvider = unimplemented!();
//! let (_sent_len, recv_len) = secrets.transcript().len();
//!
//! // First, we decide which application data we would like to disclose.
//! let mut builder = secrets.transcript_proof_builder();
//!
//! builder
//! // Use transcript encoding commitments.
//! .commitment_kinds(&[TranscriptCommitmentKind::Encoding])
//! // Disclose the first 10 bytes of the sent data.
//! .reveal(&(0..10), Direction::Sent)?
//! // Disclose all of the received data.
//! .reveal(&(0..recv_len), Direction::Received)?;
//!
//! let transcript_proof = builder.build()?;
//!
//! // Most cases we will also disclose the server identity.
//! let identity_proof = secrets.identity_proof();
//!
//! // Now we can construct the presentation.
//! let mut builder = attestation.presentation_builder(&crypto_provider);
//!
//! builder
//! .identity_proof(identity_proof)
//! .transcript_proof(transcript_proof);
//!
//! // Finally, we build the presentation. Send it to a verifier!
//! let presentation: Presentation = builder.build()?;
//! # Ok(())
//! # }
//! ```
//!
//! # Verifying a presentation
//!
//! Verifying a presentation is as simple as checking the verifier trusts the
//! verifying key then calling
//! [`Presentation::verify`](crate::presentation::Presentation::verify).
//!
//! ```no_run
//! # use tlsn_core::presentation::{Presentation, PresentationOutput};
//! # use tlsn_core::signing::VerifyingKey;
//! # use tlsn_core::CryptoProvider;
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let presentation: Presentation = unimplemented!();
//! # let trusted_key: VerifyingKey = unimplemented!();
//! # let crypto_provider: CryptoProvider = unimplemented!();
//! // Assert that we trust the verifying key.
//! assert_eq!(presentation.verifying_key(), &trusted_key);
//!
//! let PresentationOutput {
//! attestation,
//! server_name,
//! connection_info,
//! transcript,
//! ..
//! } = presentation.verify(&crypto_provider)?;
//! # Ok(())
//! # }
//! ```
#![deny(missing_docs, unreachable_pub, unused_must_use)]
#![deny(clippy::all)]
#![forbid(unsafe_code)]
pub mod attestation;
pub mod connection;
#[cfg(any(test, feature = "fixtures"))]
pub mod fixtures;
pub mod hash;
pub(crate) mod merkle;
pub mod presentation;
mod provider;
pub mod request;
mod secrets;
pub(crate) mod serialize;
pub mod signing;
pub mod merkle;
pub mod transcript;
pub use provider::CryptoProvider;
pub use secrets::Secrets;
use rangeset::ToRangeSet;
use serde::{Deserialize, Serialize};

View File

@@ -5,10 +5,10 @@ use utils::iter::DuplicateCheck;
use crate::hash::{Hash, HashAlgId, HashAlgorithm, TypedHash};
/// Errors that can occur during operations with Merkle tree and Merkle proof.
/// Merkle tree error.
#[derive(Debug, thiserror::Error)]
#[error("merkle error: {0}")]
pub(crate) struct MerkleError(String);
pub struct MerkleError(String);
impl MerkleError {
fn new(msg: impl Into<String>) -> Self {
@@ -16,8 +16,9 @@ impl MerkleError {
}
}
/// Merkle proof.
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct MerkleProof {
pub struct MerkleProof {
alg: HashAlgId,
leaf_count: usize,
proof: rs_merkle::MerkleProof<Hash>,
@@ -33,7 +34,7 @@ impl MerkleProof {
///
/// - If the length of `leaf_indices` and `leaf_hashes` does not match.
/// - If `leaf_indices` contains duplicates.
pub(crate) fn verify(
pub fn verify(
&self,
hasher: &dyn HashAlgorithm,
root: &TypedHash,
@@ -80,25 +81,29 @@ impl rs_merkle::Hasher for RsMerkleHasher<'_> {
}
}
/// Merkle tree.
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct MerkleTree {
pub struct MerkleTree {
alg: HashAlgId,
tree: rs_merkle::MerkleTree<Hash>,
}
impl MerkleTree {
pub(crate) fn new(alg: HashAlgId) -> Self {
/// Creates a new Merkle tree.
pub fn new(alg: HashAlgId) -> Self {
Self {
alg,
tree: Default::default(),
}
}
pub(crate) fn algorithm(&self) -> HashAlgId {
/// Returns the hash algorithm used to create the tree.
pub fn algorithm(&self) -> HashAlgId {
self.alg
}
pub(crate) fn root(&self) -> TypedHash {
/// Returns the root of the tree.
pub fn root(&self) -> TypedHash {
TypedHash {
alg: self.alg,
value: self.tree.root().expect("tree should not be empty"),
@@ -111,7 +116,7 @@ impl MerkleTree {
///
/// - If the provided hasher is not the same as the one used to create the
/// tree.
pub(crate) fn insert(&mut self, hasher: &dyn HashAlgorithm, mut leaves: Vec<Hash>) {
pub fn insert(&mut self, hasher: &dyn HashAlgorithm, mut leaves: Vec<Hash>) {
assert_eq!(self.alg, hasher.id(), "hash algorithm mismatch");
self.tree.append(&mut leaves);
@@ -124,7 +129,7 @@ impl MerkleTree {
///
/// - If the provided indices are not unique and sorted.
/// - If the provided indices are out of bounds.
pub(crate) fn proof(&self, indices: &[usize]) -> MerkleProof {
pub fn proof(&self, indices: &[usize]) -> MerkleProof {
assert!(
indices.windows(2).all(|w| w[0] < w[1]),
"indices must be unique and sorted"
@@ -145,7 +150,7 @@ impl MerkleTree {
#[cfg(test)]
mod test {
use crate::hash::{impl_domain_separator, Blake3, HashAlgorithmExt, Keccak256, Sha256};
use crate::hash::{Blake3, Keccak256, Sha256};
use super::*;
use rstest::*;
@@ -153,12 +158,10 @@ mod test {
#[derive(Serialize)]
struct T(u64);
impl_domain_separator!(T);
fn leaves<H: HashAlgorithm>(hasher: &H, leaves: impl IntoIterator<Item = T>) -> Vec<Hash> {
leaves
.into_iter()
.map(|x| hasher.hash_canonical(&x))
.map(|x| hasher.hash(&x.0.to_be_bytes()))
.collect()
}

View File

@@ -1,18 +0,0 @@
/// Canonical serialization of TLSNotary types.
///
/// This trait is used to serialize types into a canonical byte representation.
pub(crate) trait CanonicalSerialize {
/// Serializes the type.
fn serialize(&self) -> Vec<u8>;
}
impl<T> CanonicalSerialize for T
where
T: serde::Serialize,
{
fn serialize(&self) -> Vec<u8> {
// For now we use BCS for serialization. In future releases we will want to
// consider this further, particularly with respect to EVM compatibility.
bcs::to_bytes(self).unwrap()
}
}

View File

@@ -11,31 +11,18 @@
//! recovered by parsing the application data and relating it to the bytes
//! in the transcript.
//!
//! ## Commitments
//!
//! During the attestation process a Prover can generate multiple commitments to
//! various parts of the transcript. These commitments are inserted into the
//! attestation body and can be used by the Verifier to verify transcript proofs
//! later.
//!
//! To configure the transcript commitments, use the
//! [`TranscriptCommitConfigBuilder`].
//!
//! ## Selective Disclosure
//!
//! Using a [`TranscriptProof`] a Prover can selectively disclose parts of a
//! transcript to a Verifier in the form of a [`PartialTranscript`]. A Verifier
//! always learns the length of the transcript, but sensitive data can be
//! withheld.
//!
//! To create a proof, use the [`TranscriptProofBuilder`] which is returned by
//! [`Secrets::transcript_proof_builder`](crate::Secrets::transcript_proof_builder).
mod commit;
#[doc(hidden)]
pub mod encoding;
pub mod hash;
mod proof;
mod tls;
use std::{fmt, ops::Range};
@@ -51,6 +38,7 @@ pub use commit::{
pub use proof::{
TranscriptProof, TranscriptProofBuilder, TranscriptProofBuilderError, TranscriptProofError,
};
pub use tls::{Record, TlsTranscript};
/// A transcript contains the plaintext of all application data communicated
/// between the Prover and the Server.

View File

@@ -6,7 +6,7 @@ use rangeset::ToRangeSet;
use serde::{Deserialize, Serialize};
use crate::{
hash::{impl_domain_separator, HashAlgId},
hash::HashAlgId,
transcript::{
encoding::{EncodingCommitment, EncodingTree},
hash::{PlaintextHash, PlaintextHashSecret},
@@ -55,8 +55,6 @@ pub enum TranscriptCommitment {
Hash(PlaintextHash),
}
impl_domain_separator!(TranscriptCommitment);
/// Secret for a transcript commitment.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
@@ -67,8 +65,6 @@ pub enum TranscriptSecret {
Hash(PlaintextHashSecret),
}
impl_domain_separator!(TranscriptSecret);
/// Configuration for transcript commitments.
#[derive(Debug, Clone)]
pub struct TranscriptCommitConfig {
@@ -283,7 +279,7 @@ impl fmt::Display for TranscriptCommitConfigBuilderError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())

View File

@@ -1,7 +1,4 @@
//! Transcript encoding commitments and proofs.
//!
//! This is an internal module that is not intended to be used directly by
//! users.
mod encoder;
mod proof;
@@ -15,7 +12,7 @@ pub use tree::{EncodingTree, EncodingTreeError};
use serde::{Deserialize, Serialize};
use crate::hash::{impl_domain_separator, TypedHash};
use crate::hash::TypedHash;
/// Transcript encoding commitment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -25,5 +22,3 @@ pub struct EncodingCommitment {
/// Seed used to generate the encodings.
pub secret: EncoderSecret,
}
impl_domain_separator!(EncodingCommitment);

View File

@@ -4,14 +4,13 @@ use rangeset::{RangeSet, UnionMut};
use serde::{Deserialize, Serialize};
use crate::{
hash::{Blinder, HashProviderError},
hash::{Blinder, HashProvider, HashProviderError},
merkle::{MerkleError, MerkleProof},
transcript::{
commit::MAX_TOTAL_COMMITTED_DATA,
encoding::{new_encoder, Encoder, EncodingCommitment},
Direction, Idx,
},
CryptoProvider,
};
/// An opening of a leaf in the encoding tree.
@@ -42,18 +41,18 @@ impl EncodingProof {
///
/// # Arguments
///
/// * `provider` - Crypto provider.
/// * `provider` - Hash provider.
/// * `commitment` - Encoding commitment to verify against.
/// * `sent` - Sent data to authenticate.
/// * `recv` - Received data to authenticate.
pub fn verify_with_provider(
&self,
provider: &CryptoProvider,
provider: &HashProvider,
commitment: &EncodingCommitment,
sent: &[u8],
recv: &[u8],
) -> Result<(Idx, Idx), EncodingProofError> {
let hasher = provider.hash.get(&commitment.root.alg)?;
let hasher = provider.get(&commitment.root.alg)?;
let encoder = new_encoder(&commitment.secret);
let Self {
@@ -161,7 +160,7 @@ impl fmt::Display for EncodingProofError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())
@@ -280,7 +279,7 @@ mod test {
let err = proof
.verify_with_provider(
&CryptoProvider::default(),
&HashProvider::default(),
&commitment,
transcript.sent(),
transcript.received(),
@@ -302,7 +301,7 @@ mod test {
let recv = &transcript.received()[transcript.received().len() - 2..];
let err = proof
.verify_with_provider(&CryptoProvider::default(), &commitment, sent, recv)
.verify_with_provider(&HashProvider::default(), &commitment, sent, recv)
.unwrap_err();
assert!(matches!(err.kind, ErrorKind::Proof));
@@ -322,7 +321,7 @@ mod test {
let err = proof
.verify_with_provider(
&CryptoProvider::default(),
&HashProvider::default(),
&commitment,
transcript.sent(),
transcript.received(),
@@ -346,7 +345,7 @@ mod test {
let err = proof
.verify_with_provider(
&CryptoProvider::default(),
&HashProvider::default(),
&commitment,
transcript.sent(),
transcript.received(),

View File

@@ -13,6 +13,7 @@ pub trait EncodingProvider {
) -> Result<(), EncodingProviderError>;
}
/// Error for [`EncodingProvider`].
#[derive(Debug, thiserror::Error)]
#[error("failed to provide encoding")]
pub struct EncodingProviderError;

View File

@@ -193,9 +193,8 @@ mod tests {
use super::*;
use crate::{
fixtures::{encoder_secret, encoding_provider},
hash::Blake3,
hash::{Blake3, HashProvider},
transcript::{encoding::EncodingCommitment, Transcript},
CryptoProvider,
};
use tlsn_data_fixtures::http::{request::POST_JSON, response::OK_JSON};
@@ -229,7 +228,7 @@ mod tests {
let (auth_sent, auth_recv) = proof
.verify_with_provider(
&CryptoProvider::default(),
&HashProvider::default(),
&commitment,
transcript.sent(),
transcript.received(),
@@ -267,7 +266,7 @@ mod tests {
let (auth_sent, auth_recv) = proof
.verify_with_provider(
&CryptoProvider::default(),
&HashProvider::default(),
&commitment,
transcript.sent(),
transcript.received(),

View File

@@ -3,7 +3,7 @@
use serde::{Deserialize, Serialize};
use crate::{
hash::{impl_domain_separator, Blinder, HashAlgId, HashAlgorithm, TypedHash},
hash::{Blinder, HashAlgId, HashAlgorithm, TypedHash},
transcript::{Direction, Idx},
};
@@ -28,8 +28,6 @@ pub struct PlaintextHash {
pub hash: TypedHash,
}
impl_domain_separator!(PlaintextHash);
/// Secret component of [`PlaintextHash`].
#[derive(Clone, Serialize, Deserialize)]
pub struct PlaintextHashSecret {

View File

@@ -6,14 +6,13 @@ use std::{collections::HashSet, fmt};
use crate::{
connection::TranscriptLength,
hash::HashAlgId,
hash::{HashAlgId, HashProvider},
transcript::{
commit::{TranscriptCommitment, TranscriptCommitmentKind},
encoding::{EncodingProof, EncodingProofError, EncodingTree},
hash::{hash_plaintext, PlaintextHash, PlaintextHashSecret},
Direction, Idx, PartialTranscript, Transcript, TranscriptSecret,
},
CryptoProvider,
};
/// Default commitment kinds in order of preference for building transcript
@@ -42,11 +41,11 @@ impl TranscriptProof {
///
/// # Arguments
///
/// * `provider` - The crypto provider to use for verification.
/// * `provider` - The hash provider to use for verification.
/// * `attestation_body` - The attestation body to verify against.
pub fn verify_with_provider<'a>(
self,
provider: &CryptoProvider,
provider: &HashProvider,
length: &TranscriptLength,
commitments: impl IntoIterator<Item = &'a TranscriptCommitment>,
) -> Result<PartialTranscript, TranscriptProofError> {
@@ -109,7 +108,7 @@ impl TranscriptProof {
blinder,
} in self.hash_secrets
{
let hasher = provider.hash.get(&alg).map_err(|_| {
let hasher = provider.get(&alg).map_err(|_| {
TranscriptProofError::new(
ErrorKind::Hash,
format!("hash opening has unknown algorithm: {alg}"),
@@ -200,7 +199,7 @@ impl fmt::Display for TranscriptProofError {
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())
@@ -261,7 +260,7 @@ pub struct TranscriptProofBuilder<'a> {
impl<'a> TranscriptProofBuilder<'a> {
/// Creates a new proof builder.
pub(crate) fn new(
pub fn new(
transcript: &'a Transcript,
secrets: impl IntoIterator<Item = &'a TranscriptSecret>,
) -> Self {
@@ -546,14 +545,13 @@ impl fmt::Display for TranscriptProofBuilderError {
BuilderErrorKind::Index => f.write_str("index error")?,
BuilderErrorKind::MissingCommitment => f.write_str("commitment error")?,
BuilderErrorKind::Cover { uncovered, kinds } => f.write_str(&format!(
"unable to cover the following ranges in transcript using available {:?} commitments: {uncovered}",
kinds
"unable to cover the following ranges in transcript using available {kinds:?} commitments: {uncovered}"
))?,
BuilderErrorKind::NotSupported => f.write_str("not supported")?,
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
write!(f, " caused by: {source}")?;
}
Ok(())
@@ -569,7 +567,7 @@ mod tests {
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
use crate::{
fixtures::{encoding_provider, request_fixture, ConnectionFixture, RequestFixture},
fixtures::encoding_provider,
hash::{Blake3, Blinder, HashAlgId},
transcript::TranscriptCommitConfigBuilder,
};
@@ -579,15 +577,13 @@ mod tests {
#[rstest]
fn test_verify_missing_encoding_commitment_root() {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { encoding_tree, .. } = request_fixture(
transcript.clone(),
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection.clone(),
Blake3::default(),
Vec::new(),
);
let idxs = vec![(Direction::Received, Idx::new(0..transcript.len().1))];
let encoding_tree = EncodingTree::new(
&Blake3::default(),
&idxs,
&encoding_provider(transcript.sent(), transcript.received()),
)
.unwrap();
let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
@@ -596,7 +592,7 @@ mod tests {
let transcript_proof = builder.build().unwrap();
let provider = CryptoProvider::default();
let provider = HashProvider::default();
let err = transcript_proof
.verify_with_provider(&provider, &transcript.length(), &[])
.err()
@@ -638,14 +634,14 @@ mod tests {
#[rstest]
fn test_reveal_with_hash_commitment() {
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
let provider = CryptoProvider::default();
let provider = HashProvider::default();
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let direction = Direction::Sent;
let idx = Idx::new(0..10);
let blinder: Blinder = rng.random();
let alg = HashAlgId::SHA256;
let hasher = provider.hash.get(&alg).unwrap();
let hasher = provider.get(&alg).unwrap();
let commitment = PlaintextHash {
direction,
@@ -684,14 +680,14 @@ mod tests {
#[rstest]
fn test_reveal_with_inconsistent_hash_commitment() {
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
let provider = CryptoProvider::default();
let provider = HashProvider::default();
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let direction = Direction::Sent;
let idx = Idx::new(0..10);
let blinder: Blinder = rng.random();
let alg = HashAlgId::SHA256;
let hasher = provider.hash.get(&alg).unwrap();
let hasher = provider.get(&alg).unwrap();
let commitment = PlaintextHash {
direction,
@@ -904,7 +900,7 @@ mod tests {
assert_eq!(uncovered.recv, Idx(uncovered_recv_rangeset));
}
}
_ => panic!("unexpected error kind: {:?}", kind),
_ => panic!("unexpected error kind: {kind:?}"),
}
}
}

View File

@@ -0,0 +1,328 @@
//! TLS transcript.
use crate::{
connection::{
Certificate, HandshakeData, HandshakeDataV1_2, ServerEphemKey, ServerSignature, TlsVersion,
VerifyData,
},
transcript::{Direction, Transcript},
};
use tls_core::msgs::{
alert::AlertMessagePayload,
codec::{Codec, Reader},
enums::{AlertDescription, ContentType, ProtocolVersion},
handshake::{HandshakeMessagePayload, HandshakePayload},
};
/// A transcript of TLS records sent and received by the prover.
#[derive(Debug, Clone)]
pub struct TlsTranscript {
time: u64,
version: TlsVersion,
server_cert_chain: Option<Vec<Certificate>>,
server_signature: Option<ServerSignature>,
handshake_data: HandshakeData,
sent: Vec<Record>,
recv: Vec<Record>,
}
impl TlsTranscript {
/// Creates a new TLS transcript.
#[allow(clippy::too_many_arguments)]
pub fn new(
time: u64,
version: TlsVersion,
server_cert_chain: Option<Vec<Certificate>>,
server_signature: Option<ServerSignature>,
handshake_data: HandshakeData,
verify_data: VerifyData,
sent: Vec<Record>,
recv: Vec<Record>,
) -> Result<Self, TlsTranscriptError> {
let mut sent_iter = sent.iter();
let mut recv_iter = recv.iter();
// Make sure the client finished verify data message was sent first.
if let Some(record) = sent_iter.next() {
let payload = record
.plaintext
.as_ref()
.ok_or(TlsTranscriptError::validation(
"client finished message was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload =
HandshakeMessagePayload::read_version(&mut reader, ProtocolVersion::TLSv1_2)
.ok_or(TlsTranscriptError::validation(
"first record sent was not a handshake message",
))?;
let HandshakePayload::Finished(vd) = payload.payload else {
return Err(TlsTranscriptError::validation(
"first record sent was not a client finished message",
));
};
if vd.0 != verify_data.client_finished {
return Err(TlsTranscriptError::validation(
"inconsistent client finished verify data",
));
}
} else {
return Err(TlsTranscriptError::validation(
"client finished was not sent",
));
}
// Make sure the server finished verify data message was received first.
if let Some(record) = recv_iter.next() {
let payload = record
.plaintext
.as_ref()
.ok_or(TlsTranscriptError::validation(
"server finished message was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload =
HandshakeMessagePayload::read_version(&mut reader, ProtocolVersion::TLSv1_2)
.ok_or(TlsTranscriptError::validation(
"first record received was not a handshake message",
))?;
let HandshakePayload::Finished(vd) = payload.payload else {
return Err(TlsTranscriptError::validation(
"first record received was not a server finished message",
));
};
if vd.0 != verify_data.server_finished {
return Err(TlsTranscriptError::validation(
"inconsistent server finished verify data",
));
}
} else {
return Err(TlsTranscriptError::validation(
"server finished was not received",
));
}
// Verify last record sent was either application data or close notify.
if let Some(record) = sent_iter.next_back() {
match record.typ {
ContentType::ApplicationData => {}
ContentType::Alert => {
// Ensure the alert is a close notify.
let payload =
record
.plaintext
.as_ref()
.ok_or(TlsTranscriptError::validation(
"alert content was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload = AlertMessagePayload::read(&mut reader).ok_or(
TlsTranscriptError::validation("alert message was malformed"),
)?;
let AlertDescription::CloseNotify = payload.description else {
return Err(TlsTranscriptError::validation(
"sent alert that is not close notify",
));
};
}
typ => {
return Err(TlsTranscriptError::validation(format!(
"sent unexpected record content type: {typ:?}"
)))
}
}
}
// Verify last record received was either application data or close notify.
if let Some(record) = recv_iter.next_back() {
match record.typ {
ContentType::ApplicationData => {}
ContentType::Alert => {
// Ensure the alert is a close notify.
let payload =
record
.plaintext
.as_ref()
.ok_or(TlsTranscriptError::validation(
"alert content was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload = AlertMessagePayload::read(&mut reader).ok_or(
TlsTranscriptError::validation("alert message was malformed"),
)?;
let AlertDescription::CloseNotify = payload.description else {
return Err(TlsTranscriptError::validation(
"received alert that is not close notify",
));
};
}
typ => {
return Err(TlsTranscriptError::validation(format!(
"received unexpected record content type: {typ:?}"
)))
}
}
}
// Ensure all other records were application data.
for record in sent_iter {
if record.typ != ContentType::ApplicationData {
return Err(TlsTranscriptError::validation(format!(
"sent unexpected record content type: {:?}",
record.typ
)));
}
}
for record in recv_iter {
if record.typ != ContentType::ApplicationData {
return Err(TlsTranscriptError::validation(format!(
"received unexpected record content type: {:?}",
record.typ
)));
}
}
Ok(Self {
time,
version,
server_cert_chain,
server_signature,
handshake_data,
sent,
recv,
})
}
/// Returns the start time of the connection.
pub fn time(&self) -> u64 {
self.time
}
/// Returns the TLS protocol version.
pub fn version(&self) -> &TlsVersion {
&self.version
}
/// Returns the server certificate chain.
pub fn server_cert_chain(&self) -> Option<&[Certificate]> {
self.server_cert_chain.as_deref()
}
/// Returns the server signature.
pub fn server_signature(&self) -> Option<&ServerSignature> {
self.server_signature.as_ref()
}
/// Returns the server ephemeral key used in the TLS handshake.
pub fn server_ephemeral_key(&self) -> &ServerEphemKey {
match &self.handshake_data {
HandshakeData::V1_2(HandshakeDataV1_2 {
server_ephemeral_key,
..
}) => server_ephemeral_key,
}
}
/// Returns the handshake data.
pub fn handshake_data(&self) -> &HandshakeData {
&self.handshake_data
}
/// Returns the sent records.
pub fn sent(&self) -> &[Record] {
&self.sent
}
/// Returns the received records.
pub fn recv(&self) -> &[Record] {
&self.recv
}
/// Returns the application data transcript.
pub fn to_transcript(&self) -> Result<Transcript, TlsTranscriptError> {
let mut sent = Vec::new();
let mut recv = Vec::new();
for record in self
.sent
.iter()
.filter(|record| record.typ == ContentType::ApplicationData)
{
let plaintext = record
.plaintext
.as_ref()
.ok_or(ErrorRepr::Incomplete {
direction: Direction::Sent,
seq: record.seq,
})?
.clone();
sent.extend_from_slice(&plaintext);
}
for record in self
.recv
.iter()
.filter(|record| record.typ == ContentType::ApplicationData)
{
let plaintext = record
.plaintext
.as_ref()
.ok_or(ErrorRepr::Incomplete {
direction: Direction::Received,
seq: record.seq,
})?
.clone();
recv.extend_from_slice(&plaintext);
}
Ok(Transcript::new(sent, recv))
}
}
/// A TLS record.
#[derive(Clone)]
pub struct Record {
/// Sequence number.
pub seq: u64,
/// Content type.
pub typ: ContentType,
/// Plaintext.
pub plaintext: Option<Vec<u8>>,
/// Explicit nonce.
pub explicit_nonce: Vec<u8>,
/// Ciphertext.
pub ciphertext: Vec<u8>,
/// Tag.
pub tag: Option<Vec<u8>>,
}
opaque_debug::implement!(Record);
#[derive(Debug, thiserror::Error)]
#[error("TLS transcript error: {0}")]
pub struct TlsTranscriptError(#[from] ErrorRepr);
impl TlsTranscriptError {
fn validation(msg: impl Into<String>) -> Self {
Self(ErrorRepr::Validation(msg.into()))
}
}
#[derive(Debug, thiserror::Error)]
enum ErrorRepr {
#[error("validation error: {0}")]
Validation(String),
#[error("incomplete transcript ({direction}): seq {seq}")]
Incomplete { direction: Direction, seq: u64 },
}

View File

@@ -1,3 +1,2 @@
// Ignore files from examples.
*.tlsn
*.sh
*.tlsn

View File

@@ -8,12 +8,8 @@ version = "0.0.0"
workspace = true
[dependencies]
notary-client = { workspace = true }
notary-common = { workspace = true }
tlsn-common = { workspace = true }
tlsn-core = { workspace = true }
tlsn-prover = { workspace = true }
tlsn-verifier = { workspace = true }
tlsn = { workspace = true }
tlsn-formats = { workspace = true }
tlsn-tls-core = { workspace = true }
tls-server-fixture = { workspace = true }
@@ -21,7 +17,6 @@ tlsn-server-fixture = { workspace = true }
tlsn-server-fixture-certs = { workspace = true }
spansy = { workspace = true }
async-tungstenite = { workspace = true, features = ["tokio-runtime"] }
bincode = { workspace = true }
chrono = { workspace = true }
clap = { version = "4.5", features = ["derive"] }
@@ -32,7 +27,6 @@ hex = { workspace = true }
hyper = { workspace = true, features = ["client", "http1"] }
hyper-util = { workspace = true, features = ["full"] }
k256 = { workspace = true, features = ["ecdsa"] }
rangeset = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = [
"rt",
@@ -45,25 +39,7 @@ tokio = { workspace = true, features = [
tokio-util = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
uuid = { workspace = true, features = ["v4", "fast-rng"] }
ws_stream_tungstenite = { workspace = true, features = ["tokio_io"] }
[[example]]
name = "attestation_prove"
path = "attestation/prove.rs"
[[example]]
name = "attestation_present"
path = "attestation/present.rs"
[[example]]
name = "attestation_verify"
path = "attestation/verify.rs"
[[example]]
name = "interactive"
path = "interactive/interactive.rs"
[[example]]
name = "plugin"
path = "plugin/plugin.rs"

View File

@@ -1,111 +0,0 @@
## 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. 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
Before starting the notarization, set up the local test server and local notary.
Run the following commands from the root of this repository (not from this example's folder):
1. Run the test server:
```shell
RUST_LOG=info PORT=4000 cargo run --bin tlsn-server-fixture
```
2. Run the notary server:
```shell
cargo run --release --bin notary-server
```
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
RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example attestation_prove
```
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: 200 OK
Notarization complete!
Notarization completed successfully!
The attestation has been written to `example-json.attestation.tlsn` and the corresponding secrets to `example-json.secrets.tlsn`.
```
⚠️ 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
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, youll see this output in the console:
```log
Presentation built successfully!
The presentation has been written to `example-json.presentation.tlsn`.
```
You can create multiple presentations from the attestation and secrets in the notarization step, each with customized data redactions. You are invited to experiment!
### 3. Verify the Presentation
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
```
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 test-server.io at { time }.
Note that the data which the Prover chose not to disclose are shown as X.
Data sent:
...
```
⚠️ 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.
### HTML
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

@@ -1,117 +0,0 @@
// This example demonstrates how to build a verifiable presentation from an
// attestation and the corresponding connection secrets. See the `prove.rs`
// example to learn how to acquire an attestation from a Notary.
use clap::Parser;
use hyper::header;
use tlsn_core::{attestation::Attestation, presentation::Presentation, CryptoProvider, Secrets};
use tlsn_examples::ExampleType;
use tlsn_formats::http::HttpTranscript;
#[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(attestation_path)?)?;
// Read secrets from disk.
let secrets: Secrets = bincode::deserialize(&std::fs::read(secrets_path)?)?;
// Parse the HTTP transcript.
let transcript = HttpTranscript::parse(secrets.transcript())?;
// Build a transcript proof.
let mut builder = secrets.transcript_proof_builder();
// Here is where we reveal all or some of the parts we committed in `prove.rs`
// previously.
let request = &transcript.requests[0];
// Reveal the structure of the request without the headers or body.
builder.reveal_sent(&request.without_data())?;
// Reveal the request target.
builder.reveal_sent(&request.request.target)?;
// Reveal all request headers except the values of User-Agent and Authorization.
for header in &request.headers {
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 only parts of the response.
let response = &transcript.responses[0];
// Reveal the structure of the response without the headers or body.
builder.reveal_recv(&response.without_data())?;
// Reveal all response headers.
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()?;
// Use default crypto provider to build the presentation.
let provider = CryptoProvider::default();
let mut builder = attestation.presentation_builder(&provider);
builder
.identity_proof(secrets.identity_proof())
.transcript_proof(transcript_proof);
let presentation: Presentation = builder.build()?;
let presentation_path = tlsn_examples::get_file_path(example_type, "presentation");
// Write the presentation to disk.
std::fs::write(&presentation_path, bincode::serialize(&presentation)?)?;
println!("Presentation built successfully!");
println!("The presentation has been written to `{presentation_path}`.");
Ok(())
}

View File

@@ -1,247 +0,0 @@
// This example demonstrates how to use the Prover to acquire an attestation for
// an HTTP request sent to example.com. The attestation and secrets are saved to
// disk.
use std::env;
use clap::Parser;
use http_body_util::Empty;
use hyper::{body::Bytes, Request, StatusCode};
use hyper_util::rt::TokioIo;
use spansy::Spanned;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::debug;
use notary_client::{Accepted, NotarizationRequest, NotaryClient};
use tls_core::verify::WebPkiVerifier;
use tls_server_fixture::{CA_CERT_DER, SERVER_DOMAIN};
use tlsn_common::config::ProtocolConfig;
use tlsn_core::{request::RequestConfig, transcript::TranscriptCommitConfig, CryptoProvider};
use tlsn_examples::ExampleType;
use tlsn_formats::http::{DefaultHttpCommitter, HttpCommit, HttpTranscript};
use tlsn_prover::{Prover, ProverConfig, TlsConfig};
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
use tlsn_server_fixture_certs::{CLIENT_CERT, CLIENT_KEY};
// Setting of the application server.
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 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);
// 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.");
// Create a crypto provider accepting the server-fixture's self-signed
// root certificate.
//
// This is only required for offline testing with the server-fixture. In
// production, use `CryptoProvider::default()` instead.
let mut root_store = tls_core::anchors::RootCertStore::empty();
root_store
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
// Set up protocol configuration for prover.
let mut prover_config_builder = ProverConfig::builder();
prover_config_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(tlsn_examples::MAX_SENT_DATA)
.max_recv_data(tlsn_examples::MAX_RECV_DATA)
.build()?,
)
.crypto_provider(crypto_provider);
// (Optional) Set up TLS client authentication if required by the server.
prover_config_builder.tls_config(
TlsConfig::builder()
.client_auth_pem((vec![CLIENT_CERT.to_vec()], CLIENT_KEY.to_vec()))
.unwrap()
.build()?,
);
let prover_config = prover_config_builder.build()?;
// Create a new prover and perform necessary setup.
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_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
// data written to/read from it will be encrypted/decrypted using MPC with
// the notary.
let (mpc_tls_connection, prover_fut) = prover.connect(client_socket.compat()).await?;
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
// Spawn the prover task to be run concurrently in the background.
let prover_task = tokio::spawn(prover_fut);
// Attach the hyper HTTP client to the connection.
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(mpc_tls_connection).await?;
// Spawn the HTTP task to be run concurrently in the background.
tokio::spawn(connection);
// Build a simple HTTP request with common headers.
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);
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: {}", response.status());
assert!(response.status() == StatusCode::OK);
// The prover task should be done now, so we can await it.
let mut prover = prover_task.await??;
// Parse the HTTP transcript.
let transcript = HttpTranscript::parse(prover.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());
// This commits to various parts of the transcript separately (e.g. request
// headers, response headers, response body and more). See https://docs.tlsnotary.org//protocol/commit_strategy.html
// for other strategies that can be used to generate commitments.
DefaultHttpCommitter::default().commit_transcript(&mut builder, &transcript)?;
let transcript_commit = builder.build()?;
// Build an attestation request.
let mut builder = RequestConfig::builder();
builder.transcript_commit(transcript_commit);
// Optionally, add an extension to the attestation if the notary supports it.
// builder.extension(Extension {
// id: b"example.name".to_vec(),
// value: b"Bobert".to_vec(),
// });
let request_config = builder.build()?;
#[allow(deprecated)]
let (attestation, secrets) = prover.notarize(&request_config).await?;
println!("Notarization complete!");
// Write the attestation to disk.
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(&secrets_path, bincode::serialize(&secrets)?).await?;
println!("Notarization completed successfully!");
println!(
"The attestation has been written to `{attestation_path}` and the \
corresponding secrets to `{secrets_path}`."
);
Ok(())
}

View File

@@ -1,94 +0,0 @@
// This example demonstrates how to verify a presentation. See `present.rs` for
// an example of how to build a presentation from an attestation and connection
// secrets.
use std::time::Duration;
use clap::Parser;
use tls_core::verify::WebPkiVerifier;
use tls_server_fixture::CA_CERT_DER;
use tlsn_core::{
presentation::{Presentation, PresentationOutput},
signing::VerifyingKey,
CryptoProvider,
};
use tlsn_examples::ExampleType;
#[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)?)?;
// Create a crypto provider accepting the server-fixture's self-signed
// root certificate.
//
// This is only required for offline testing with the server-fixture. In
// production, use `CryptoProvider::default()` instead.
let mut root_store = tls_core::anchors::RootCertStore::empty();
root_store
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
let VerifyingKey {
alg,
data: key_data,
} = presentation.verifying_key();
println!(
"Verifying presentation with {alg} key: {}\n\n**Ask yourself, do you trust this key?**\n",
hex::encode(key_data)
);
// Verify the presentation.
let PresentationOutput {
server_name,
connection_info,
transcript,
// extensions, // Optionally, verify any custom extensions from prover/notary.
..
} = presentation.verify(&crypto_provider).unwrap();
// The time at which the connection was started.
let time = chrono::DateTime::UNIX_EPOCH + Duration::from_secs(connection_info.time);
let server_name = server_name.unwrap();
let mut partial_transcript = transcript.unwrap();
// Set the unauthenticated bytes so they are distinguishable.
partial_transcript.set_unauthed(b'X');
let sent = String::from_utf8_lossy(partial_transcript.sent_unsafe());
let recv = String::from_utf8_lossy(partial_transcript.received_unsafe());
println!("-------------------------------------------------------------------");
println!(
"Successfully verified that the data below came from a session with {server_name} at {time}.",
);
println!("Note that the data which the Prover chose not to disclose are shown as X.\n");
println!("Data sent:\n");
println!("{}\n", sent);
println!("Data received:\n");
println!("{}\n", recv);
println!("-------------------------------------------------------------------");
Ok(())
}

View File

@@ -10,16 +10,15 @@ use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::instrument;
use tls_core::verify::WebPkiVerifier;
use tls_server_fixture::CA_CERT_DER;
use tlsn_common::config::{ProtocolConfig, ProtocolConfigValidator};
use tlsn_core::{
transcript::PartialTranscript, CryptoProvider, ProveConfig, VerifierOutput, VerifyConfig,
use tlsn::{
config::{ProtocolConfig, ProtocolConfigValidator},
prover::{ProveConfig, Prover, ProverConfig, TlsConfig},
transcript::PartialTranscript,
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
};
use tlsn_prover::{Prover, ProverConfig, TlsConfig};
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
use tlsn_server_fixture_certs::{CLIENT_CERT, CLIENT_KEY, SERVER_DOMAIN};
use tlsn_verifier::{Verifier, VerifierConfig};
use tlsn_server_fixture_certs::SERVER_DOMAIN;
const SECRET: &str = "TLSNotary's private key 🤡";
@@ -70,41 +69,29 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
assert_eq!(uri.scheme().unwrap().as_str(), "https");
let server_domain = uri.authority().unwrap().host();
// Create a crypto provider accepting the server-fixture's self-signed
// root certificate.
//
// This is only required for offline testing with the server-fixture. In
// production, use `CryptoProvider::default()` instead.
// Create a root certificate store with the server-fixture's self-signed
// certificate. This is only required for offline testing with the
// 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 crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
let mut tls_config_builder = TlsConfig::builder();
tls_config_builder.root_store(root_store);
let tls_config = tls_config_builder.build().unwrap();
// Set up protocol configuration for prover.
let mut prover_config_builder = ProverConfig::builder();
prover_config_builder
.server_name(server_domain)
.tls_config(tls_config)
.protocol_config(
ProtocolConfig::builder()
.max_sent_data(MAX_SENT_DATA)
.max_recv_data(MAX_RECV_DATA)
.build()
.unwrap(),
)
.crypto_provider(crypto_provider);
// (Optional) Set up TLS client authentication if required by the server.
prover_config_builder.tls_config(
TlsConfig::builder()
.client_auth_pem((vec![CLIENT_CERT.to_vec()], CLIENT_KEY.to_vec()))
.unwrap()
.build()
.unwrap(),
);
);
let prover_config = prover_config_builder.build().unwrap();
@@ -204,23 +191,17 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
.build()
.unwrap();
// Create a crypto provider accepting the server-fixture's self-signed
// root certificate.
//
// This is only required for offline testing with the server-fixture. In
// production, use `CryptoProvider::default()` instead.
// Create a root certificate store with the server-fixture's self-signed
// certificate. This is only required for offline testing with the
// 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 crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
let verifier_config = VerifierConfig::builder()
.root_store(root_store)
.protocol_config_validator(config_validator)
.crypto_provider(crypto_provider)
.build()
.unwrap();
let verifier = Verifier::new(verifier_config);
@@ -243,14 +224,14 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
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));
.unwrap_or_else(|| panic!("Verification failed: Expected host {SERVER_DOMAIN}"));
// Check received data.
let received = transcript.received_unsafe().to_vec();
let response = String::from_utf8(received.clone()).expect("Verifier expected received data");
response
.find("Herman Melville")
.unwrap_or_else(|| panic!("Expected valid data from {}", SERVER_DOMAIN));
.unwrap_or_else(|| panic!("Expected valid data from {SERVER_DOMAIN}"));
// Check Session info: server name.
assert_eq!(server_name.as_str(), SERVER_DOMAIN);

View File

@@ -1,302 +0,0 @@
use async_tungstenite::{tokio::connect_async_with_config, tungstenite::protocol::WebSocketConfig};
use clap::Parser;
use http_body_util::{BodyExt, Empty, Full};
use hyper::{body::Bytes, Request, StatusCode, Uri};
use hyper_util::{
client::legacy::connect::HttpConnector,
rt::{TokioExecutor, TokioIo},
};
use notary_client::{Accepted, NotarizationRequest, NotaryClient};
use notary_common::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse};
use rangeset::RangeSet;
use spansy::{
http::parse_response,
json::{self},
Spanned,
};
use std::env;
use tlsn_common::config::ProtocolConfig;
use tlsn_core::{
hash::HashAlgId,
transcript::{TranscriptCommitConfig, TranscriptCommitmentKind},
ProveConfig,
};
use tlsn_prover::{Prover, ProverConfig};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::{debug, info};
use ws_stream_tungstenite::WsStream;
const SECRET: &str = "TLSNotary's private key 🤡";
const SERVER_URL: &str = "https://raw.githubusercontent.com/tlsnotary/tlsn/refs/tags/v0.1.0-alpha.11/crates/server-fixture/server/src/data/1kb.json";
#[derive(clap::ValueEnum, Clone, Default, Debug)]
pub enum ProverType {
#[default]
Tcp,
Ws,
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// What data to notarize.
#[clap(default_value_t, value_enum)]
prover_type: ProverType,
/// Name of verifier plugin to use — can get available names from `/info` endpoint of verifier server.
#[clap(long, default_value = "plugin_rs")]
plugin: String,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let verifier_host: String = env::var("VERIFIER_HOST").unwrap_or("127.0.0.1".into());
let verifier_port: u16 = env::var("VERIFIER_PORT")
.map(|port| port.parse().expect("port should be valid integer"))
.unwrap_or(7047);
let args = Args::parse();
match args.prover_type {
ProverType::Tcp => run_tcp_prover(&verifier_host, verifier_port, &args.plugin).await,
ProverType::Ws => run_ws_prover(&verifier_host, verifier_port, &args.plugin).await,
}
}
async fn run_ws_prover(verifier_host: &str, verifier_port: u16, plugin_name: &str) {
info!("Running websocket prover...");
// Build the HTTP request to configure notarization
let payload = serde_json::to_string(&NotarizationSessionRequest {
client_type: ClientType::Websocket,
plugin: plugin_name.to_string(),
max_sent_data: Some(tlsn_examples::MAX_SENT_DATA),
max_recv_data: Some(tlsn_examples::MAX_RECV_DATA),
})
.unwrap();
let session_request = Request::builder()
.uri(format!("http://{verifier_host}:{verifier_port}/session"))
.method("POST")
.header("Host", verifier_host)
// Need to specify application/json for axum to parse it as json
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(payload)))
.unwrap();
let hyper_http_connector: HttpConnector = HttpConnector::new();
let http_client =
hyper_util::client::legacy::Builder::new(TokioExecutor::new()).build(hyper_http_connector);
let response = http_client.request(session_request).await.unwrap();
assert!(response.status() == StatusCode::OK);
let payload = response.into_body().collect().await.unwrap().to_bytes();
let notarization_response =
serde_json::from_str::<NotarizationSessionResponse>(&String::from_utf8_lossy(&payload))
.unwrap();
debug!("Session request response: {:?}", notarization_response,);
let verification_request = hyper::Request::builder()
.uri(format!(
"ws://{verifier_host}:{verifier_port}/notarize?sessionId={}",
notarization_response.session_id
))
.header("Host", verifier_host)
.header("Sec-WebSocket-Key", uuid::Uuid::new_v4().to_string())
.header("Sec-WebSocket-Version", "13")
.header("Connection", "Upgrade")
.header("Upgrade", "Websocket")
.body(())
.unwrap();
let (verifier_ws_stream, _) =
connect_async_with_config(verification_request, Some(WebSocketConfig::default()))
.await
.unwrap();
info!("Websocket connection established!");
let verifier_ws_socket = WsStream::new(verifier_ws_stream);
prover(verifier_ws_socket, SERVER_URL).await;
info!("Websocket proving is successful!");
}
async fn run_tcp_prover(verifier_host: &str, verifier_port: u16, plugin_name: &str) {
info!("Running tcp prover...");
// Build a tcp client to connect to the verifier server.
let verifier_client = NotaryClient::builder()
.host(verifier_host)
.port(verifier_port)
// WARNING: Always use TLS to connect to verifier server, except if verifier 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 verification to the verifier server.
let verification_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)
.plugin(plugin_name.to_string())
.build()
.unwrap();
let Accepted {
io: verifier_connection,
id: _session_id,
..
} = verifier_client
.request_notarization(verification_request)
.await
.expect("Could not connect to verifier. Make sure it is running.");
info!("Tcp connection established!");
prover(verifier_connection, SERVER_URL).await;
info!("Tcp proving is successful!");
}
async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(verifier_socket: T, uri: &str) {
debug!("Starting proving...");
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.
//
// Perform the setup phase with the verifier.
let prover = Prover::new(
ProverConfig::builder()
.server_name(server_domain)
.protocol_config(
ProtocolConfig::builder()
.max_sent_data(tlsn_examples::MAX_SENT_DATA)
.max_recv_data(tlsn_examples::MAX_RECV_DATA)
.build()
.unwrap(),
)
.build()
.unwrap(),
)
.setup(verifier_socket.compat())
.await
.unwrap();
// Connect to TLS Server.
let tls_client_socket = tokio::net::TcpStream::connect((server_domain, server_port))
.await
.unwrap();
// Pass server connection into the prover.
let (mpc_tls_connection, prover_fut) =
prover.connect(tls_client_socket.compat()).await.unwrap();
// Wrap the connection in a TokioIo compatibility layer to use it with hyper.
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
// Spawn the Prover to run in the background.
let prover_task = tokio::spawn(prover_fut);
// MPC-TLS Handshake.
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(mpc_tls_connection)
.await
.unwrap();
tokio::spawn(connection);
// MPC-TLS: Send Request and wait for Response.
info!("Send Request and wait for Response");
let request = Request::builder()
.uri(uri.clone())
.header("Host", server_domain)
.header("Connection", "close")
.header("Secret", SECRET)
.method("GET")
.body(Empty::<Bytes>::new())
.unwrap();
let response = request_sender.send_request(request).await.unwrap();
debug!("TLS response: {:?}", response);
assert!(response.status() == StatusCode::OK);
// Create proof for the Verifier.
let mut prover = prover_task.await.unwrap().unwrap();
let (sent_len, recv_len) = prover.transcript().len();
let mut builder = TranscriptCommitConfig::builder(prover.transcript());
builder.default_kind(TranscriptCommitmentKind::Hash {
alg: HashAlgId::SHA256,
});
builder.commit_sent(&(0..sent_len)).unwrap();
builder.commit_recv(&(0..recv_len)).unwrap();
let transcript_commit = builder.build().unwrap();
let mut builder: tlsn_core::ProveConfigBuilder<'_> = ProveConfig::builder(prover.transcript());
// Reveal the DNS name.
builder.server_identity();
let sent_rangeset = redact_and_reveal_sent_data(prover.transcript().sent());
let _ = builder.reveal_sent(&sent_rangeset);
let recv_rangeset = redact_and_reveal_received_data(prover.transcript().received());
let _ = builder.reveal_recv(&recv_rangeset);
builder.transcript_commit(transcript_commit);
let config = builder.build().unwrap();
prover.prove(&config).await.unwrap();
prover.close().await.unwrap();
}
/// Redacts and reveals received data to the verifier.
fn redact_and_reveal_received_data(recv_transcript: &[u8]) -> RangeSet<usize> {
// Get the some information from the received data.
let received_string = String::from_utf8(recv_transcript.to_vec()).unwrap();
debug!("Received data: {}", received_string);
let resp = parse_response(recv_transcript).unwrap();
let body = resp.body.unwrap();
let mut json = json::parse_slice(body.as_bytes()).unwrap();
json.offset(body.content.span().indices().min().unwrap());
let name = json.get("information.name").expect("name field not found");
let street = json
.get("information.address.street")
.expect("street field not found");
let name_start = name.span().indices().min().unwrap() - 9; // 9 is the length of "name: "
let name_end = name.span().indices().max().unwrap() + 1; // include `"`
let street_start = street.span().indices().min().unwrap() - 11; // 11 is the length of "street: "
let street_end = street.span().indices().max().unwrap() + 1; // include `"`
[name_start..name_end + 1, street_start..street_end + 1].into()
}
/// Redacts and reveals sent data to the verifier.
fn redact_and_reveal_sent_data(sent_transcript: &[u8]) -> RangeSet<usize> {
let sent_transcript_len = sent_transcript.len();
let sent_string: String = String::from_utf8(sent_transcript.to_vec()).unwrap();
let secret_start = sent_string.find(SECRET).unwrap();
debug!("Send data: {}", sent_string);
// Reveal everything except for the SECRET.
[
0..secret_start,
secret_start + SECRET.len()..sent_transcript_len,
]
.into()
}

View File

@@ -15,11 +15,11 @@ pub enum ExampleType {
impl fmt::Display for ExampleType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
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)
format!("example-{example_type}.{content_type}.tlsn")
}

View File

@@ -1,6 +1,6 @@
[package]
name = "tlsn-formats"
version = "0.1.0-alpha.12"
version = "0.1.0-alpha.13-pre"
edition = "2021"
[lints]

View File

@@ -1,4 +1,4 @@
*.svg
*.html
bin/
/bin/

View File

@@ -2,6 +2,15 @@
A harness for testing and benchmarking the TLSNotary protocol with both native and browser support.
## Installation
The harness requires the nightly compiler to build the WASM binary. Additionally, we depend on a specific
version of `wasm-pack` which must be installed:
```bash
cargo install --git https://github.com/rustwasm/wasm-pack.git --rev 32e52ca
```
## Getting started
First build the harness.

View File

@@ -0,0 +1,2 @@
# exclude any /target folders
**/target/

View File

@@ -3,7 +3,7 @@
# Ensure the script runs in the folder that contains this script
cd "$(dirname "$0")"
cargo build --release --package tlsn-harness-runner --package tlsn-harness-executor --package tlsn-server-fixture
cargo build --release --package tlsn-harness-runner --package tlsn-harness-executor --package tlsn-server-fixture --package tlsn-harness-plot
mkdir -p bin
@@ -11,5 +11,6 @@ cp ../../target/release/tlsn-harness-runner bin/runner
cp ../../target/release/tlsn-harness-executor-native bin/executor-native
cp ../../target/release/tlsn-server-fixture bin/server-fixture
cp ../../target/release/tlsn-harness-wasm-server bin/wasm-server
cp ../../target/release/tlsn-harness-plot bin/tlsn-harness-plot
./build.wasm.sh
./build.wasm.sh

View File

@@ -3,5 +3,12 @@
# Ensure the script runs in the folder that contains this script
cd "$(dirname "$0")"
# A specific version of `wasm-pack` must be installed to build the WASM binary
cargo install --git https://github.com/rustwasm/wasm-pack.git --rev 32e52ca
rustup run nightly \
wasm-pack build executor --target web --no-pack --out-dir=../static/generated -- -Zbuild-std=panic_abort,std
wasm-pack build executor \
--profile wasm \
--target web \
--out-dir=../static/generated \
-- -Zbuild-std=panic_abort,std

18
crates/harness/docker.md Normal file
View File

@@ -0,0 +1,18 @@
# Run the TLSN benches with Docker
In the root folder of this repository, run:
```
docker build --pull -t tlsn-bench . -f ./crates/harness/harness.Dockerfile
```
Next run the benches with:
```
docker run -it --privileged -v ./crates/harness/:/benches tlsn-bench bash -c "runner setup; runner bench"
```
The `--privileged` parameter is required because this test bench needs permission to create networks with certain parameters
To run the benches in a browser run:
```
docker run -it --privileged -v ./crates/harness/:/benches tlsn-bench bash -c "cd /; runner setup; runner --target browser bench"
```

View File

@@ -1,7 +1,7 @@
[target.wasm32-unknown-unknown]
rustflags = [
"-C",
"target-feature=+atomics,+bulk-memory,+mutable-globals",
"target-feature=+atomics,+bulk-memory,+mutable-globals,+simd128",
"-C",
# 4GB
"link-arg=--max-memory=4294967296",

View File

@@ -8,12 +8,13 @@ publish = false
name = "harness_executor"
crate-type = ["cdylib", "rlib"]
[package.metadata.wasm-pack.profile.custom]
wasm-opt = ["-O3"]
[dependencies]
tlsn-harness-core = { workspace = true }
tlsn-prover = { workspace = true }
tlsn-common = { workspace = true }
tlsn = { workspace = true }
tlsn-core = { workspace = true }
tlsn-verifier = { workspace = true }
tlsn-tls-core = { workspace = true }
tlsn-server-fixture-certs = { workspace = true }

View File

@@ -4,10 +4,10 @@ use anyhow::Result;
use futures::{AsyncReadExt, AsyncWriteExt, TryFutureExt};
use harness_core::bench::{Bench, ProverMetrics};
use tls_core::verify::WebPkiVerifier;
use tlsn_common::config::ProtocolConfig;
use tlsn_core::{CryptoProvider, ProveConfig};
use tlsn_prover::{Prover, ProverConfig};
use tlsn::{
config::ProtocolConfig,
prover::{ProveConfig, Prover, ProverConfig, TlsConfig},
};
use tlsn_server_fixture_certs::{CA_CERT_DER, SERVER_DOMAIN};
use crate::{
@@ -37,16 +37,15 @@ pub async fn bench_prover(provider: &IoProvider, config: &Bench) -> Result<Prove
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
let mut tls_config_builder = TlsConfig::builder();
tls_config_builder.root_store(root_store);
let tls_config = tls_config_builder.build()?;
let prover = Prover::new(
ProverConfig::builder()
.tls_config(tls_config)
.protocol_config(protocol_config)
.server_name(SERVER_DOMAIN)
.crypto_provider(crypto_provider)
.build()?,
);
@@ -71,10 +70,10 @@ pub async fn bench_prover(provider: &IoProvider, config: &Bench) -> Result<Prove
);
conn.write_all(request.as_bytes()).await?;
conn.close().await?;
let mut response = Vec::new();
conn.read_to_end(&mut response).await?;
conn.close().await?;
Ok(())
},

View File

@@ -1,11 +1,11 @@
use anyhow::Result;
use harness_core::bench::Bench;
use tls_core::verify::WebPkiVerifier;
use tlsn_common::config::ProtocolConfigValidator;
use tlsn_core::{CryptoProvider, VerifyConfig};
use tlsn::{
config::ProtocolConfigValidator,
verifier::{Verifier, VerifierConfig, VerifyConfig},
};
use tlsn_server_fixture_certs::CA_CERT_DER;
use tlsn_verifier::{Verifier, VerifierConfig};
use crate::{IoProvider, bench::RECV_PADDING};
@@ -22,15 +22,10 @@ pub async fn bench_verifier(provider: &IoProvider, config: &Bench) -> Result<()>
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
let verifier = Verifier::new(
VerifierConfig::builder()
.root_store(root_store)
.protocol_config_validator(protocol_config)
.crypto_provider(crypto_provider)
.build()?,
);

View File

@@ -58,14 +58,14 @@ impl Executor {
Role::Prover => {
let metrics = bench_prover(&provider, &config)
.await
.map_err(|e| RpcError::new(format!("prover bench failed: {}", e)))?;
.map_err(|e| RpcError::new(format!("prover bench failed: {e}")))?;
Ok(CmdOutput::Bench(BenchOutput::Prover { metrics }))
}
Role::Verifier => {
bench_verifier(&provider, &config)
.await
.map_err(|e| RpcError::new(format!("verifier bench failed: {}", e)))?;
.map_err(|e| RpcError::new(format!("verifier bench failed: {e}")))?;
Ok(CmdOutput::Bench(BenchOutput::Verifier))
}

View File

@@ -1,13 +1,12 @@
use tls_core::{anchors::RootCertStore, verify::WebPkiVerifier};
use tlsn_common::config::{ProtocolConfig, ProtocolConfigValidator};
use tlsn_core::{
CryptoProvider, ProveConfig, VerifierOutput, VerifyConfig,
use tls_core::anchors::RootCertStore;
use tlsn::{
config::{ProtocolConfig, ProtocolConfigValidator},
hash::HashAlgId,
prover::{ProveConfig, Prover, ProverConfig, TlsConfig},
transcript::{TranscriptCommitConfig, TranscriptCommitment, TranscriptCommitmentKind},
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
};
use tlsn_prover::{Prover, ProverConfig};
use tlsn_server_fixture_certs::{CA_CERT_DER, SERVER_DOMAIN};
use tlsn_verifier::{Verifier, VerifierConfig};
use http_body_util::{BodyExt as _, Empty};
use hyper::{Request, StatusCode, body::Bytes};
@@ -27,14 +26,15 @@ async fn prover(provider: &IoProvider) {
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
let mut tls_config_builder = TlsConfig::builder();
tls_config_builder.root_store(root_store);
let tls_config = tls_config_builder.build().unwrap();
let prover = Prover::new(
ProverConfig::builder()
.server_name(SERVER_DOMAIN)
.tls_config(tls_config)
.protocol_config(
ProtocolConfig::builder()
.max_sent_data(MAX_SENT_DATA)
@@ -43,7 +43,6 @@ async fn prover(provider: &IoProvider) {
.build()
.unwrap(),
)
.crypto_provider(crypto_provider)
.build()
.unwrap(),
)
@@ -120,11 +119,6 @@ async fn verifier(provider: &IoProvider) {
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
.unwrap();
let crypto_provider = CryptoProvider {
cert: WebPkiVerifier::new(root_store, None),
..Default::default()
};
let config = VerifierConfig::builder()
.protocol_config_validator(
ProtocolConfigValidator::builder()
@@ -133,7 +127,7 @@ async fn verifier(provider: &IoProvider) {
.build()
.unwrap(),
)
.crypto_provider(crypto_provider)
.root_store(root_store)
.build()
.unwrap();

View File

@@ -0,0 +1,33 @@
FROM rust AS builder
WORKDIR /usr/src/tlsn
RUN \
rustup update; \
apt update && apt install -y clang; \
rustup install nightly; \
rustup component add rust-src --toolchain nightly; \
cargo install --git https://github.com/rustwasm/wasm-pack.git --rev 32e52ca;
COPY . .
RUN \
cd crates/harness; \
./build.sh;
FROM debian:latest
RUN apt update && apt upgrade -y && apt install -y --no-install-recommends \
chromium \
iproute2 \
sudo \
procps \
iptables; \
apt clean && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/tlsn/crates/harness/bin/ /usr/local/bin/
COPY --from=builder /usr/src/tlsn/crates/harness/static /static
# RUN /usr/local/bin/runner setup
VOLUME [ "/benches" ]
WORKDIR "/benches"
# CMD ["/usr/local/bin/runner", "test"]

View File

@@ -0,0 +1,19 @@
[package]
name = "tlsn-harness-plot"
version = "0.0.0"
edition = "2024"
publish = false
[dependencies]
tlsn-harness-core = { workspace = true }
# tlsn-server-fixture = { workspace = true }
charming = { version = "0.5.1", features = ["ssr"] }
csv = "1.3.0"
clap = { workspace = true, features = ["derive", "env"] }
itertools = "0.14.0"
toml = { workspace = true }
[[bin]]
name = "tlsn-harness-plot"
path = "bin/plot.rs"

View File

@@ -0,0 +1,277 @@
use std::f32;
use charming::{
Chart, HtmlRenderer,
component::{Axis, Legend, Title},
element::{AreaStyle, LineStyle, NameLocation, Orient, TextStyle, Tooltip, Trigger},
series::Line,
theme::Theme,
};
use clap::Parser;
use harness_core::bench::{BenchItems, Measurement};
use itertools::Itertools;
const THEME: Theme = Theme::Default;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Cli {
/// Path to the Bench.toml file with benchmark spec
toml: String,
/// Path to the CSV file with benchmark results
csv: String,
/// Prover kind: native or browser
#[arg(short, long, value_enum, default_value = "native")]
prover_kind: ProverKind,
/// Add min/max bands to plots
#[arg(long, default_value_t = false)]
min_max_band: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum ProverKind {
Native,
Browser,
}
impl std::fmt::Display for ProverKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProverKind::Native => write!(f, "Native"),
ProverKind::Browser => write!(f, "Browser"),
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let mut rdr = csv::Reader::from_path(&cli.csv)?;
let items: BenchItems = toml::from_str(&std::fs::read_to_string(&cli.toml)?)?;
let groups = items.group;
// Prepare data for plotting.
let all_data: Vec<Measurement> = rdr
.deserialize::<Measurement>()
.collect::<Result<Vec<_>, _>>()?;
for group in groups {
if group.protocol_latency.is_some() {
let latency = group.protocol_latency.unwrap();
plot_runtime_vs(
&all_data,
cli.min_max_band,
&group.name,
|r| r.bandwidth as f32 / 1000.0, // Kbps to Mbps
"Runtime vs Bandwidth",
format!("{} ms Latency, {} mode", latency, cli.prover_kind),
"runtime_vs_bandwidth.html",
"Bandwidth (Mbps)",
)?;
}
if group.bandwidth.is_some() {
let bandwidth = group.bandwidth.unwrap();
plot_runtime_vs(
&all_data,
cli.min_max_band,
&group.name,
|r| r.latency as f32,
"Runtime vs Latency",
format!("{} bps bandwidth, {} mode", bandwidth, cli.prover_kind),
"runtime_vs_latency.html",
"Latency (ms)",
)?;
}
}
Ok(())
}
struct DataPoint {
min: f32,
mean: f32,
max: f32,
}
struct Points {
preprocess: DataPoint,
online: DataPoint,
total: DataPoint,
}
#[allow(clippy::too_many_arguments)]
fn plot_runtime_vs<Fx>(
all_data: &[Measurement],
show_min_max: bool,
group: &str,
x_value: Fx,
title: &str,
subtitle: String,
output_file: &str,
x_axis_label: &str,
) -> Result<Chart, Box<dyn std::error::Error>>
where
Fx: Fn(&Measurement) -> f32,
{
fn data_point(values: &[f32]) -> DataPoint {
let mean = values.iter().copied().sum::<f32>() / values.len() as f32;
let max = values.iter().copied().reduce(f32::max).unwrap_or_default();
let min = values.iter().copied().reduce(f32::min).unwrap_or_default();
DataPoint { min, mean, max }
}
let stats: Vec<(f32, Points)> = all_data
.iter()
.filter(|r| r.group.as_deref() == Some(group))
.map(|r| {
(
x_value(r),
r.time_preprocess as f32 / 1000.0, // ms to s
r.time_online as f32 / 1000.0,
r.time_total as f32 / 1000.0,
)
})
.sorted_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
.chunk_by(|entry| entry.0)
.into_iter()
.map(|(x, group)| {
let group_vec: Vec<_> = group.collect();
let preprocess = data_point(
&group_vec
.iter()
.map(|(_, t, _, _)| *t)
.collect::<Vec<f32>>(),
);
let online = data_point(
&group_vec
.iter()
.map(|(_, _, t, _)| *t)
.collect::<Vec<f32>>(),
);
let total = data_point(
&group_vec
.iter()
.map(|(_, _, _, t)| *t)
.collect::<Vec<f32>>(),
);
(
x,
Points {
preprocess,
online,
total,
},
)
})
.collect();
let mut chart = Chart::new()
.title(
Title::new()
.text(title)
.left("center")
.subtext(subtitle)
.subtext_style(TextStyle::new().font_size(16)),
)
.tooltip(Tooltip::new().trigger(Trigger::Axis))
.legend(
Legend::new()
.data(vec!["Preprocess Mean", "Online Mean", "Total Mean"])
.top("80")
.right("110")
.orient(Orient::Vertical)
.item_gap(10),
)
.x_axis(
Axis::new()
.name(x_axis_label)
.scale(true)
.name_location(NameLocation::Middle)
.name_gap(30)
.name_text_style(TextStyle::new().font_size(21)),
)
.y_axis(
Axis::new()
.name("Time (seconds)")
.scale(true)
.name_location(NameLocation::Middle)
.name_rotation(90)
.name_gap(30)
.name_text_style(TextStyle::new().font_size(21)),
);
chart = add_mean_series(chart, &stats, "Preprocess Mean", |p| p.preprocess.mean);
chart = add_mean_series(chart, &stats, "Online Mean", |p| p.online.mean);
chart = add_mean_series(chart, &stats, "Total Mean", |p| p.total.mean);
if show_min_max {
chart = add_min_max_band(
chart,
&stats,
"Preprocess Min/Max",
|p| &p.preprocess,
"#ccc",
);
chart = add_min_max_band(chart, &stats, "Online Min/Max", |p| &p.online, "#ccc");
chart = add_min_max_band(chart, &stats, "Total Min/Max", |p| &p.total, "#ccc");
}
// Save the chart as HTML file.
HtmlRenderer::new(title, 1000, 800)
.theme(THEME)
.save(&chart, output_file)
.unwrap();
Ok(chart)
}
fn add_mean_series(
chart: Chart,
stats: &[(f32, Points)],
name: &str,
extract: impl Fn(&Points) -> f32,
) -> Chart {
chart.series(
Line::new()
.name(name)
.data(
stats
.iter()
.map(|(x, points)| vec![*x, extract(points)])
.collect(),
)
.symbol_size(6),
)
}
fn add_min_max_band(
chart: Chart,
stats: &[(f32, Points)],
name: &str,
extract: impl Fn(&Points) -> &DataPoint,
color: &str,
) -> Chart {
chart.series(
Line::new()
.name(name)
.data(
stats
.iter()
.map(|(x, points)| vec![*x, extract(points).max])
.chain(
stats
.iter()
.rev()
.map(|(x, points)| vec![*x, extract(points).min]),
)
.collect(),
)
.show_symbol(false)
.line_style(LineStyle::new().opacity(0.0))
.area_style(AreaStyle::new().opacity(0.3).color(color)),
)
}

View File

@@ -111,6 +111,7 @@ impl Executor {
chrome_path,
format!("--remote-debugging-port={PORT_BROWSER}"),
"--headless",
"--disable-dev-shm-usage",
"--disable-gpu",
"--disable-cache",
"--disable-application-cache",
@@ -142,13 +143,13 @@ impl Executor {
tokio::spawn(async move {
while let Some(res) = handler.next().await {
if let Err(e) = res {
eprintln!("chromium error: {:?}", e);
eprintln!("chromium error: {e:?}");
}
}
});
let page = browser
.new_page(&format!("http://{}:{}/index.html", wasm_addr, wasm_port))
.new_page(&format!("http://{wasm_addr}:{wasm_port}/index.html"))
.await?;
page.execute(EnableParams::builder().build()).await?;

View File

@@ -342,8 +342,8 @@ struct VethPair(Veth, Veth);
impl VethPair {
fn new(name: &str) -> Self {
Self(
Veth::new(&format!("{}-0", name)),
Veth::new(&format!("{}-1", name)),
Veth::new(&format!("{name}-0")),
Veth::new(&format!("{name}-1")),
)
}

View File

@@ -79,6 +79,10 @@ pub async fn main() -> Result<()> {
HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("cache-control"),
HeaderValue::from_static("no-store"),
))
.service(files);
// build our application with a single route

View File

@@ -10,7 +10,7 @@ class Executor {
await initWasm();
console.log("wasm loaded");
console.log("initializing wasm");
await wasm.initialize({ thread_count: navigator.hardwareConcurrency });
await wasm.initialize(undefined, navigator.hardwareConcurrency);
console.log("wasm initialized");
console.log("initializing executor");
this.executor = new wasm.WasmExecutor(config);

View File

@@ -5,7 +5,7 @@ description = "TLSNotary MPC-TLS protocol"
keywords = ["tls", "mpc", "2pc"]
categories = ["cryptography"]
license = "MIT OR Apache-2.0"
version = "0.1.0-alpha.12"
version = "0.1.0-alpha.13-pre"
edition = "2021"
[lints]
@@ -19,11 +19,11 @@ default = []
[dependencies]
tlsn-cipher = { workspace = true }
tlsn-common = { workspace = true }
tlsn-hmac-sha256 = { workspace = true }
tlsn-key-exchange = { workspace = true }
tlsn-tls-backend = { workspace = true }
tlsn-tls-core = { workspace = true, features = ["serde"] }
tlsn-core = { workspace = true }
mpz-common = { workspace = true }
mpz-core = { workspace = true }
@@ -54,6 +54,7 @@ ctr = { workspace = true }
ghash_rc = { package = "ghash", version = "0.5" }
tokio = { workspace = true, features = ["sync"] }
pin-project-lite = { workspace = true }
web-time = { workspace = true }
[dev-dependencies]
mpz-ole = { workspace = true, features = ["test-utils"] }

View File

@@ -1,7 +1,7 @@
use crate::{
msg::Message,
msg::{Message, StartHandshake},
record_layer::{aead::MpcAesGcm, RecordLayer},
Config, FollowerData, MpcTlsError, Role, SessionKeys, Vm,
Config, MpcTlsError, Role, SessionKeys, Vm,
};
use hmac_sha256::{MpcPrf, PrfOutput};
use ke::KeyExchange;
@@ -20,15 +20,16 @@ use mpz_ot::{
use mpz_share_conversion::{ShareConversionReceiver, ShareConversionSender};
use serio::stream::IoStreamExt;
use std::mem;
use tls_core::msgs::{
alert::AlertMessagePayload,
codec::{Codec, Reader},
enums::{AlertDescription, ContentType, NamedGroup, ProtocolVersion},
handshake::{HandshakeMessagePayload, HandshakePayload},
use tls_core::msgs::enums::NamedGroup;
use tlsn_core::{
connection::{HandshakeData, HandshakeDataV1_2, TlsVersion, VerifyData},
transcript::TlsTranscript,
};
use tlsn_common::transcript::TlsTranscript;
use tracing::{debug, instrument};
// Maximum handshake time difference in seconds.
const MAX_TIME_DIFF: u64 = 5;
/// MPC-TLS follower.
#[derive(Debug)]
pub struct MpcTlsFollower {
@@ -143,7 +144,6 @@ impl MpcTlsFollower {
self.state = State::Setup {
vm,
keys: keys.clone(),
ke,
prf,
record_layer,
@@ -159,12 +159,12 @@ impl MpcTlsFollower {
pub async fn preprocess(&mut self) -> Result<(), MpcTlsError> {
let State::Setup {
vm,
keys,
mut ke,
prf,
mut record_layer,
cf_vd,
sf_vd,
..
} = self.state.take()
else {
return Err(MpcTlsError::state("must be in setup state to preprocess"));
@@ -203,7 +203,6 @@ impl MpcTlsFollower {
self.state = State::Ready {
vm,
keys,
ke,
prf,
record_layer,
@@ -216,20 +215,21 @@ impl MpcTlsFollower {
/// Runs the follower.
#[instrument(skip_all, err)]
pub async fn run(mut self) -> Result<(Context, FollowerData), MpcTlsError> {
pub async fn run(mut self) -> Result<(Context, TlsTranscript), MpcTlsError> {
let State::Ready {
vm,
keys,
mut ke,
mut prf,
mut record_layer,
cf_vd: mut cf_vd_fut,
sf_vd: mut sf_vd_fut,
..
} = self.state.take()
else {
return Err(MpcTlsError::state("must be in ready state to run"));
};
let mut time = None;
let mut client_random = None;
let mut server_random = None;
let mut server_key = None;
@@ -244,7 +244,23 @@ impl MpcTlsFollower {
}
prf.set_client_random(random.random)?;
client_random = Some(random);
client_random = Some(random.random);
}
Message::StartHandshake(StartHandshake { time: prover_time }) => {
if time.is_some() {
return Err(MpcTlsError::hs("time already set"));
}
let this_time = web_time::UNIX_EPOCH
.elapsed()
.expect("system time is available")
.as_secs();
if prover_time.abs_diff(this_time) > MAX_TIME_DIFF {
return Err(MpcTlsError::hs("handshake time difference exceeds limit"));
}
time = Some(prover_time);
}
Message::SetServerRandom(random) => {
if server_random.is_some() {
@@ -252,7 +268,7 @@ impl MpcTlsFollower {
}
prf.set_server_random(random.random)?;
server_random = Some(random);
server_random = Some(random.random);
}
Message::SetServerKey(key) => {
if server_key.is_some() {
@@ -378,24 +394,41 @@ impl MpcTlsFollower {
debug!("committing");
let transcript = record_layer.commit(&mut self.ctx, vm).await?;
let (sent_records, recv_records) = record_layer.commit(&mut self.ctx, vm).await?;
debug!("committed");
let time = time.ok_or(MpcTlsError::hs("time was not set"))?;
let server_key = server_key.ok_or(MpcTlsError::hs("server key not set"))?;
let client_random = client_random.ok_or(MpcTlsError::hs("client random not set"))?;
let server_random = server_random.ok_or(MpcTlsError::hs("client random not set"))?;
let cf_vd = cf_vd.ok_or(MpcTlsError::hs("client finished VD not computed"))?;
let sf_vd = sf_vd.ok_or(MpcTlsError::hs("server finished VD not computed"))?;
validate_transcript(cf_vd, sf_vd, &transcript)?;
let handshake_data = HandshakeData::V1_2(HandshakeDataV1_2 {
client_random,
server_random,
server_ephemeral_key: server_key
.try_into()
.expect("only supported key scheme should have been accepted"),
});
Ok((
self.ctx,
FollowerData {
server_key,
transcript,
keys,
let transcript = TlsTranscript::new(
time,
TlsVersion::V1_2,
None,
None,
handshake_data,
VerifyData {
client_finished: cf_vd.to_vec(),
server_finished: sf_vd.to_vec(),
},
))
sent_records,
recv_records,
)
.map_err(MpcTlsError::other)?;
Ok((self.ctx, transcript))
}
}
@@ -408,7 +441,6 @@ enum State {
},
Setup {
vm: Vm,
keys: SessionKeys,
ke: Box<dyn KeyExchange + Send + Sync + 'static>,
prf: MpcPrf,
record_layer: RecordLayer,
@@ -417,7 +449,6 @@ enum State {
},
Ready {
vm: Vm,
keys: SessionKeys,
ke: Box<dyn KeyExchange + Send + Sync + 'static>,
prf: MpcPrf,
record_layer: RecordLayer,
@@ -443,149 +474,3 @@ impl std::fmt::Debug for State {
}
}
}
fn validate_transcript(
cf_vd: [u8; 12],
sf_vd: [u8; 12],
transcript: &TlsTranscript,
) -> Result<(), MpcTlsError> {
let mut sent = transcript.sent.iter();
let mut recv = transcript.recv.iter();
// Make sure the client finished verify data message was consistent.
if let Some(record) = sent.next() {
let payload = record.plaintext.as_ref().ok_or(MpcTlsError::record_layer(
"client finished message was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload = HandshakeMessagePayload::read_version(&mut reader, ProtocolVersion::TLSv1_2)
.ok_or(MpcTlsError::record_layer(
"first record sent was not a handshake message",
))?;
let HandshakePayload::Finished(actual_cf_vd) = payload.payload else {
return Err(MpcTlsError::record_layer(
"first record sent was not a client finished message",
));
};
if cf_vd != actual_cf_vd.0.as_slice() {
return Err(MpcTlsError::record_layer(format!(
"client finished verify data does not match output from PRF: {:?} != {:?}",
cf_vd, actual_cf_vd
)));
}
} else {
return Err(MpcTlsError::record_layer("client finished was not sent"));
}
// Make sure the server finished verify data message was consistent.
if let Some(record) = recv.next() {
let payload = record.plaintext.as_ref().ok_or(MpcTlsError::record_layer(
"server finished message was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload = HandshakeMessagePayload::read_version(&mut reader, ProtocolVersion::TLSv1_2)
.ok_or(MpcTlsError::record_layer(
"first record received was not a handshake message",
))?;
let HandshakePayload::Finished(actual_sf_vd) = payload.payload else {
return Err(MpcTlsError::record_layer(
"first record received was not a server finished message",
));
};
if sf_vd != actual_sf_vd.0.as_slice() {
return Err(MpcTlsError::record_layer(format!(
"server finished verify data does not match output from PRF: {:?} != {:?}",
sf_vd, actual_sf_vd
)));
}
} else {
return Err(MpcTlsError::record_layer(
"server finished was not received",
));
}
// Verify last record sent was either application data or close notify.
if let Some(record) = sent.next_back() {
match record.typ {
ContentType::ApplicationData => {}
ContentType::Alert => {
// Ensure the alert is a close notify.
let payload = record.plaintext.as_ref().ok_or(MpcTlsError::record_layer(
"alert content was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload = AlertMessagePayload::read(&mut reader)
.ok_or(MpcTlsError::record_layer("alert message was malformed"))?;
let AlertDescription::CloseNotify = payload.description else {
return Err(MpcTlsError::record_layer(
"sent alert that is not close notify",
));
};
}
typ => {
return Err(MpcTlsError::record_layer(format!(
"sent unexpected record content type: {:?}",
typ
)))
}
}
}
// Verify last record received was either application data or close notify.
if let Some(record) = recv.next_back() {
match record.typ {
ContentType::ApplicationData => {}
ContentType::Alert => {
// Ensure the alert is a close notify.
let payload = record.plaintext.as_ref().ok_or(MpcTlsError::record_layer(
"alert content was hidden from the follower",
))?;
let mut reader = Reader::init(payload);
let payload = AlertMessagePayload::read(&mut reader)
.ok_or(MpcTlsError::record_layer("alert message was malformed"))?;
let AlertDescription::CloseNotify = payload.description else {
return Err(MpcTlsError::record_layer(
"received alert that is not close notify",
));
};
}
typ => {
return Err(MpcTlsError::record_layer(format!(
"received unexpected record content type: {:?}",
typ
)))
}
}
}
// Ensure all other records were application data.
for record in sent {
if record.typ != ContentType::ApplicationData {
return Err(MpcTlsError::record_layer(format!(
"sent unexpected record content type: {:?}",
record.typ
)));
}
}
for record in recv {
if record.typ != ContentType::ApplicationData {
return Err(MpcTlsError::record_layer(format!(
"received unexpected record content type: {:?}",
record.typ
)));
}
}
Ok(())
}

View File

@@ -4,11 +4,11 @@ use crate::{
error::MpcTlsError,
msg::{
ClientFinishedVd, Decrypt, Encrypt, Message, ServerFinishedVd, SetClientRandom,
SetServerKey, SetServerRandom,
SetServerKey, SetServerRandom, StartHandshake,
},
record_layer::{aead::MpcAesGcm, DecryptMode, EncryptMode, RecordLayer},
utils::opaque_into_parts,
Config, LeaderOutput, Role, SessionKeys, Vm,
Config, Role, SessionKeys, Vm,
};
use async_trait::async_trait;
use hmac_sha256::{MpcPrf, PrfOutput};
@@ -42,6 +42,12 @@ use tls_core::{
},
suites::SupportedCipherSuite,
};
use tlsn_core::{
connection::{
Certificate, HandshakeData, HandshakeDataV1_2, ServerSignature, TlsVersion, VerifyData,
},
transcript::TlsTranscript,
};
use tracing::{debug, instrument, trace, warn};
/// Controller for MPC-TLS leader.
@@ -175,12 +181,11 @@ impl MpcTlsLeader {
self.state = State::Setup {
ctx,
vm,
keys: keys.clone(),
ke,
prf,
record_layer,
cf_vd,
sf_vd,
cf_vd_fut: cf_vd,
sf_vd_fut: sf_vd,
client_random,
};
@@ -193,13 +198,13 @@ impl MpcTlsLeader {
let State::Setup {
mut ctx,
vm,
keys,
mut ke,
mut prf,
mut record_layer,
cf_vd,
sf_vd,
cf_vd_fut,
sf_vd_fut,
client_random,
..
} = self.state.take()
else {
return Err(MpcTlsError::state("must be in setup state to preprocess"));
@@ -250,12 +255,12 @@ impl MpcTlsLeader {
self.state = State::Handshake {
ctx,
vm,
keys,
ke,
prf,
record_layer,
cf_vd,
sf_vd,
cf_vd_fut,
sf_vd_fut,
time: None,
protocol_version: None,
cipher_suite: None,
client_random,
@@ -274,10 +279,11 @@ impl MpcTlsLeader {
let State::Active {
mut ctx,
vm,
keys,
mut record_layer,
cf_vd,
sf_vd,
time,
protocol_version,
cipher_suite,
client_random,
server_random,
server_cert_details,
@@ -297,7 +303,7 @@ impl MpcTlsLeader {
debug!("committing to transcript");
let transcript = record_layer.commit(&mut ctx, vm.clone()).await?;
let (sent_records, recv_records) = record_layer.commit(&mut ctx, vm.clone()).await?;
debug!("committed to transcript");
@@ -306,21 +312,59 @@ impl MpcTlsLeader {
self.notifier.set();
}
let cf_vd = cf_vd.ok_or(MpcTlsError::state("client finished verify data not set"))?;
let sf_vd = sf_vd.ok_or(MpcTlsError::state("server finished verify data not set"))?;
let version = match protocol_version {
ProtocolVersion::TLSv1_2 => TlsVersion::V1_2,
version => {
panic!("only TLS 1.2 should have been accepted: {version:?}")
}
};
let server_cert_chain = server_cert_details
.cert_chain()
.iter()
.map(|cert| Certificate(cert.0.clone()))
.collect();
let server_signature = ServerSignature {
scheme: server_kx_details
.kx_sig()
.scheme
.try_into()
.expect("only supported signature scheme should have been accepted"),
sig: server_kx_details.kx_sig().sig.0.clone(),
};
let handshake_data = HandshakeData::V1_2(HandshakeDataV1_2 {
client_random: client_random.0,
server_random: server_random.0,
server_ephemeral_key: server_key
.try_into()
.expect("only supported key scheme should have been accepted"),
});
let transcript = TlsTranscript::new(
time,
version,
Some(server_cert_chain),
Some(server_signature),
handshake_data,
VerifyData {
client_finished: cf_vd.to_vec(),
server_finished: sf_vd.to_vec(),
},
sent_records,
recv_records,
)
.map_err(MpcTlsError::other)?;
self.state = State::Closed {
ctx,
vm,
record_layer,
data: LeaderOutput {
protocol_version,
cipher_suite,
server_key,
server_cert_details,
server_kx_details,
client_random,
server_random,
transcript,
keys,
},
transcript,
};
Ok(())
@@ -419,6 +463,7 @@ impl Backend for MpcTlsLeader {
ctx,
prf,
server_random,
time,
..
} = &mut self.state
else {
@@ -427,6 +472,18 @@ impl Backend for MpcTlsLeader {
);
};
let now = web_time::UNIX_EPOCH
.elapsed()
.expect("system time is available")
.as_secs();
*time = Some(now);
ctx.io_mut()
.send(Message::StartHandshake(StartHandshake { time: now }))
.await
.map_err(MpcTlsError::from)?;
ctx.io_mut()
.send(Message::SetServerRandom(SetServerRandom {
random: random.0,
@@ -520,6 +577,7 @@ impl Backend for MpcTlsLeader {
ctx,
vm,
prf,
sf_vd_fut,
sf_vd,
..
} = &mut self.state
@@ -552,12 +610,14 @@ impl Backend for MpcTlsLeader {
vm.execute_all(ctx).await.map_err(MpcTlsError::hs)?;
}
let sf_vd = sf_vd
let vd = sf_vd_fut
.try_recv()
.map_err(MpcTlsError::hs)?
.ok_or_else(|| MpcTlsError::hs("sf_vd is not decoded"))?;
Ok(sf_vd.to_vec())
*sf_vd = Some(vd);
Ok(vd.to_vec())
}
#[instrument(level = "debug", skip_all, err)]
@@ -566,6 +626,7 @@ impl Backend for MpcTlsLeader {
ctx,
vm,
prf,
cf_vd_fut,
cf_vd,
..
} = &mut self.state
@@ -598,12 +659,14 @@ impl Backend for MpcTlsLeader {
vm.execute_all(ctx).await.map_err(MpcTlsError::hs)?;
}
let cf_vd = cf_vd
let vd = cf_vd_fut
.try_recv()
.map_err(MpcTlsError::hs)?
.ok_or_else(|| MpcTlsError::hs("cf_vd is not decoded"))?;
Ok(cf_vd.to_vec())
*cf_vd = Some(vd);
Ok(vd.to_vec())
}
#[instrument(level = "debug", skip_all, err)]
@@ -611,19 +674,19 @@ impl Backend for MpcTlsLeader {
let State::Handshake {
mut ctx,
vm,
keys,
mut ke,
mut prf,
mut record_layer,
cf_vd,
sf_vd,
cf_vd_fut,
sf_vd_fut,
time,
protocol_version,
cipher_suite,
client_random,
server_random,
server_cert_details,
server_key,
server_kx_details,
..
} = self.state.take()
else {
return Err(
@@ -633,10 +696,9 @@ impl Backend for MpcTlsLeader {
debug!("preparing encryption");
let time = time.ok_or_else(|| MpcTlsError::hs("time is not set"))?;
let protocol_version =
protocol_version.ok_or_else(|| MpcTlsError::hs("protocol version is not set"))?;
let cipher_suite =
cipher_suite.ok_or_else(|| MpcTlsError::hs("cipher suite is not set"))?;
let server_random =
server_random.ok_or_else(|| MpcTlsError::hs("server random is not set"))?;
let server_cert_details =
@@ -676,14 +738,15 @@ impl Backend for MpcTlsLeader {
self.state = State::Active {
ctx,
vm,
keys,
_ke: ke,
prf,
record_layer,
cf_vd,
sf_vd,
cf_vd_fut,
sf_vd_fut,
cf_vd: None,
sf_vd: None,
time,
protocol_version,
cipher_suite,
client_random,
server_random,
server_cert_details,
@@ -983,23 +1046,22 @@ enum State {
Setup {
ctx: Context,
vm: Vm,
keys: SessionKeys,
ke: Box<dyn KeyExchange + Send + Sync + 'static>,
prf: MpcPrf,
record_layer: RecordLayer,
cf_vd: DecodeFutureTyped<BitVec, [u8; 12]>,
sf_vd: DecodeFutureTyped<BitVec, [u8; 12]>,
cf_vd_fut: DecodeFutureTyped<BitVec, [u8; 12]>,
sf_vd_fut: DecodeFutureTyped<BitVec, [u8; 12]>,
client_random: Random,
},
Handshake {
ctx: Context,
vm: Vm,
keys: SessionKeys,
ke: Box<dyn KeyExchange + Send + Sync + 'static>,
prf: MpcPrf,
record_layer: RecordLayer,
cf_vd: DecodeFutureTyped<BitVec, [u8; 12]>,
sf_vd: DecodeFutureTyped<BitVec, [u8; 12]>,
cf_vd_fut: DecodeFutureTyped<BitVec, [u8; 12]>,
sf_vd_fut: DecodeFutureTyped<BitVec, [u8; 12]>,
time: Option<u64>,
protocol_version: Option<ProtocolVersion>,
cipher_suite: Option<CipherSuite>,
client_random: Random,
@@ -1011,14 +1073,15 @@ enum State {
Active {
ctx: Context,
vm: Vm,
keys: SessionKeys,
_ke: Box<dyn KeyExchange + Send + Sync + 'static>,
prf: MpcPrf,
record_layer: RecordLayer,
cf_vd: DecodeFutureTyped<BitVec, [u8; 12]>,
sf_vd: DecodeFutureTyped<BitVec, [u8; 12]>,
cf_vd_fut: DecodeFutureTyped<BitVec, [u8; 12]>,
sf_vd_fut: DecodeFutureTyped<BitVec, [u8; 12]>,
cf_vd: Option<[u8; 12]>,
sf_vd: Option<[u8; 12]>,
time: u64,
protocol_version: ProtocolVersion,
cipher_suite: CipherSuite,
client_random: Random,
server_random: Random,
server_cert_details: ServerCertDetails,
@@ -1029,7 +1092,7 @@ enum State {
ctx: Context,
vm: Vm,
record_layer: RecordLayer,
data: LeaderOutput,
transcript: TlsTranscript,
},
Error,
}

View File

@@ -1,5 +1,5 @@
use crate::{
leader::{LeaderOutput, MpcTlsLeader, State},
leader::{MpcTlsLeader, State},
MpcTlsError,
};
use async_trait::async_trait;
@@ -18,6 +18,7 @@ use tls_core::{
},
suites::SupportedCipherSuite,
};
use tlsn_core::transcript::TlsTranscript;
use tracing::{debug, Instrument};
#[derive(Clone)]
@@ -69,7 +70,7 @@ impl MpcTlsLeader {
mut self,
) -> (
MpcTlsLeaderCtrl,
impl Future<Output = Result<(Context, LeaderOutput), MpcTlsError>>,
impl Future<Output = Result<(Context, TlsTranscript), MpcTlsError>>,
) {
let (mut mailbox, address) = mailbox(100);
@@ -82,17 +83,20 @@ impl MpcTlsLeader {
}
impl Actor for MpcTlsLeader {
type Stop = (Context, LeaderOutput);
type Stop = (Context, TlsTranscript);
type Error = MpcTlsError;
async fn stopped(&mut self) -> Result<Self::Stop, Self::Error> {
debug!("leader actor stopped");
let State::Closed { ctx, data, .. } = self.state.take() else {
let State::Closed {
ctx, transcript, ..
} = self.state.take()
else {
return Err(MpcTlsError::state("leader actor stopped in invalid state"));
};
Ok((ctx, data))
Ok((ctx, transcript))
}
}

View File

@@ -25,16 +25,7 @@ use mpz_memory_core::{
Array,
};
use mpz_vm_core::Vm as VmTrait;
use tls_core::{
cert::ServerCertDetails,
ke::ServerKxDetails,
key::PublicKey,
msgs::{
enums::{CipherSuite, ProtocolVersion},
handshake::Random,
},
};
use tlsn_common::transcript::TlsTranscript;
use tokio::sync::Mutex;
pub(crate) type BoxFut<T> = Pin<Box<dyn Future<Output = T> + Send + Sync + 'static>>;
@@ -61,38 +52,3 @@ pub struct SessionKeys {
/// Server write MAC key.
pub server_write_mac_key: Array<U8, 16>,
}
/// MPC-TLS Leader output.
#[derive(Debug)]
pub struct LeaderOutput {
/// TLS protocol version.
pub protocol_version: ProtocolVersion,
/// TLS cipher suite.
pub cipher_suite: CipherSuite,
/// Server ephemeral public key.
pub server_key: PublicKey,
/// Server certificate chain and related details.
pub server_cert_details: ServerCertDetails,
/// Key exchange details.
pub server_kx_details: ServerKxDetails,
/// Client random.
pub client_random: Random,
/// Server random.
pub server_random: Random,
/// TLS transcript.
pub transcript: TlsTranscript,
/// TLS session keys.
pub keys: SessionKeys,
}
/// MPC-TLS Follower output.
#[derive(Debug)]
pub struct FollowerData {
/// Server ephemeral public key.
pub server_key: PublicKey,
/// TLS transcript in which the received records are unauthenticated
/// from the follower's perspective.
pub transcript: TlsTranscript,
/// TLS session keys.
pub keys: SessionKeys,
}

View File

@@ -10,6 +10,7 @@ use crate::record_layer::{DecryptMode, EncryptMode};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Message {
SetClientRandom(SetClientRandom),
StartHandshake(StartHandshake),
SetServerRandom(SetServerRandom),
SetServerKey(SetServerKey),
ClientFinishedVd(ClientFinishedVd),
@@ -26,6 +27,11 @@ pub(crate) struct SetClientRandom {
pub(crate) random: [u8; 32],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct StartHandshake {
pub(crate) time: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SetServerRandom {
pub(crate) random: [u8; 32],

View File

@@ -21,7 +21,7 @@ use tls_core::{
cipher::make_tls12_aad,
msgs::enums::{ContentType, ProtocolVersion},
};
use tlsn_common::transcript::{Record, TlsTranscript};
use tlsn_core::transcript::Record;
use tokio::sync::Mutex;
use tracing::{debug, instrument};
@@ -489,11 +489,9 @@ impl RecordLayer {
seq: op.seq,
typ: op.typ,
plaintext: op.plaintext,
plaintext_ref: pending.plaintext_ref,
explicit_nonce: op.explicit_nonce,
ciphertext,
tag,
version: op.version,
});
}
@@ -509,11 +507,9 @@ impl RecordLayer {
seq: op.seq,
typ: op.typ,
plaintext,
plaintext_ref: None,
explicit_nonce: op.explicit_nonce,
ciphertext: op.ciphertext,
tag: Some(op.tag),
version: op.version,
});
}
@@ -526,7 +522,7 @@ impl RecordLayer {
&mut self,
ctx: &mut Context,
vm: Vm,
) -> Result<TlsTranscript, MpcTlsError> {
) -> Result<(Vec<Record>, Vec<Record>), MpcTlsError> {
let State::Online {
sent_records,
mut recv_records,
@@ -584,20 +580,15 @@ impl RecordLayer {
seq: op.seq,
typ: op.typ,
plaintext,
plaintext_ref: None,
explicit_nonce: op.explicit_nonce,
ciphertext: op.ciphertext,
tag: Some(op.tag),
version: op.version,
});
}
self.state = State::Complete {};
Ok(TlsTranscript {
sent: sent_records,
recv: recv_records,
})
Ok((sent_records, recv_records))
}
fn next_write(

View File

@@ -274,6 +274,29 @@ impl Add for TagShare {
}
}
/// Builds padded data for GHASH.
fn build_ghash_data(mut aad: Vec<u8>, mut ciphertext: Vec<u8>) -> Vec<u8> {
let associated_data_bitlen = (aad.len() as u64) * 8;
let text_bitlen = (ciphertext.len() as u64) * 8;
let len_block = ((associated_data_bitlen as u128) << 64) + (text_bitlen as u128);
// Pad data to be a multiple of 16 bytes.
let aad_padded_block_count = (aad.len() / 16) + (aad.len() % 16 != 0) as usize;
aad.resize(aad_padded_block_count * 16, 0);
let ciphertext_padded_block_count =
(ciphertext.len() / 16) + (ciphertext.len() % 16 != 0) as usize;
ciphertext.resize(ciphertext_padded_block_count * 16, 0);
let mut data: Vec<u8> = Vec::with_capacity(aad.len() + ciphertext.len() + 16);
data.extend(aad);
data.extend(ciphertext);
data.extend_from_slice(&len_block.to_be_bytes());
data
}
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub(crate) struct GhashError(#[from] ErrorRepr);

View File

@@ -4,12 +4,11 @@ use async_trait::async_trait;
use futures::{stream::FuturesOrdered, StreamExt as _};
use mpz_common::{Context, Task};
use serio::{stream::IoStreamExt, SinkExt};
use tlsn_common::ghash::build_ghash_data;
use crate::{
decode::OneTimePadShared,
record_layer::aead::{
ghash::{Ghash, TagShare},
ghash::{build_ghash_data, Ghash, TagShare},
AeadError,
},
Role,

View File

@@ -4,12 +4,11 @@ use async_trait::async_trait;
use futures::{stream::FuturesOrdered, StreamExt};
use mpz_common::{Context, Task};
use serio::{stream::IoStreamExt, SinkExt};
use tlsn_common::ghash::build_ghash_data;
use crate::{
decode::OneTimePadShared,
record_layer::aead::{
ghash::{Ghash, TagShare},
ghash::{build_ghash_data, Ghash, TagShare},
AeadError,
},
Role,

View File

@@ -1,9 +1,6 @@
use futures::TryFutureExt as _;
use mpz_core::bitvec::BitVec;
use mpz_memory_core::{
binary::{Binary, U8},
DecodeFutureTyped, Vector,
};
use mpz_memory_core::{binary::Binary, DecodeFutureTyped};
use mpz_vm_core::{prelude::*, Vm};
use serde::{Deserialize, Serialize};
use tls_core::msgs::enums::{ContentType, ProtocolVersion};
@@ -21,14 +18,7 @@ fn private(
vm: &mut dyn Vm<Binary>,
encrypter: &mut MpcAesGcm,
op: &EncryptOp,
) -> Result<
(
Vector<U8>,
EncryptOutput,
BoxFut<Result<Vec<u8>, AeadError>>,
),
MpcTlsError,
> {
) -> Result<(EncryptOutput, BoxFut<Result<Vec<u8>, AeadError>>), MpcTlsError> {
let (plaintext, ciphertext) = encrypter
.apply_keystream(vm, op.explicit_nonce.clone(), op.len)
.map_err(MpcTlsError::record_layer)?;
@@ -46,7 +36,6 @@ fn private(
);
Ok((
plaintext,
EncryptOutput::Private(EncryptPrivate {
ciphertext: vm.decode(ciphertext).map_err(MpcTlsError::record_layer)?,
}),
@@ -107,21 +96,15 @@ pub(crate) fn encrypt(
for op in ops {
match op.mode {
EncryptMode::Private => {
let (plaintext_ref, output, ciphertext_fut) = private(vm, encrypter, op)?;
let (output, ciphertext_fut) = private(vm, encrypter, op)?;
outputs.push(PendingEncrypt {
plaintext_ref: Some(plaintext_ref),
output,
});
outputs.push(PendingEncrypt { output });
ciphertext_futs.push(ciphertext_fut);
}
EncryptMode::Public => {
let (output, ciphertext_fut) = public(vm, encrypter, op)?;
outputs.push(PendingEncrypt {
plaintext_ref: None,
output,
});
outputs.push(PendingEncrypt { output });
ciphertext_futs.push(ciphertext_fut);
}
}
@@ -213,7 +196,6 @@ impl EncryptOutput {
}
pub(crate) struct PendingEncrypt {
pub(crate) plaintext_ref: Option<Vector<U8>>,
pub(crate) output: EncryptOutput,
}

View File

@@ -1,29 +0,0 @@
[package]
name = "notary-client"
version = "0.1.0-alpha.12"
edition = "2021"
[lints]
workspace = true
[dependencies]
notary-common = { workspace = true }
derive_builder = { workspace = true }
futures = { workspace = true }
http-body-util = { workspace = true }
hyper = { workspace = true, features = ["client", "http1"] }
hyper-util = { workspace = true, features = ["full"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = [
"rt",
"rt-multi-thread",
"macros",
"net",
"io-std",
"fs",
] }
tokio-rustls = { workspace = true }
tracing = { workspace = true }
webpki-roots = { workspace = true }

View File

@@ -1,535 +0,0 @@
//! Notary client.
//!
//! This module sets up connection to notary server via TCP or TLS for
//! subsequent requests for notarization.
use http_body_util::{BodyExt as _, Either, Empty, Full};
use hyper::{
body::{Bytes, Incoming},
client::conn::http1::Parts,
header::AUTHORIZATION,
Request, Response, StatusCode,
};
use hyper_util::rt::TokioIo;
use notary_common::{
ClientType, NotarizationSessionRequest, NotarizationSessionResponse, X_API_KEY_HEADER,
};
use std::{
io::Error as IoError,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::TcpStream,
time::{sleep, timeout, Duration},
};
use tokio_rustls::{
client::TlsStream,
rustls::{self, ClientConfig, OwnedTrustAnchor, RootCertStore},
TlsConnector,
};
use tracing::{debug, error, info};
use crate::error::{ClientError, ErrorKind};
/// Parameters used to configure notarization.
#[derive(Debug, Clone, derive_builder::Builder)]
pub struct NotarizationRequest {
/// Name of verifier plugin that prover wants to interact with.
plugin: String,
/// Maximum number of bytes that can be sent.
max_sent_data: usize,
/// Maximum number of bytes that can be received.
max_recv_data: usize,
}
impl NotarizationRequest {
/// Creates a new builder for `NotarizationRequest`.
pub fn builder() -> NotarizationRequestBuilder {
NotarizationRequestBuilder::default()
}
}
/// An accepted notarization request.
#[derive(Debug)]
#[non_exhaustive]
pub struct Accepted {
/// Session identifier.
pub id: String,
/// Connection to the notary server to be used by a prover.
pub io: NotaryConnection,
}
/// A notary server connection.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum NotaryConnection {
/// Unencrypted TCP connection.
Tcp(TcpStream),
/// TLS connection.
Tls(TlsStream<TcpStream>),
}
impl AsyncRead for NotaryConnection {
#[inline]
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<Result<(), IoError>> {
match self.get_mut() {
NotaryConnection::Tcp(stream) => Pin::new(stream).poll_read(cx, buf),
NotaryConnection::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
}
}
}
impl AsyncWrite for NotaryConnection {
#[inline]
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, IoError>> {
match self.get_mut() {
NotaryConnection::Tcp(stream) => Pin::new(stream).poll_write(cx, buf),
NotaryConnection::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
}
}
#[inline]
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
match self.get_mut() {
NotaryConnection::Tcp(stream) => Pin::new(stream).poll_flush(cx),
NotaryConnection::Tls(stream) => Pin::new(stream).poll_flush(cx),
}
}
#[inline]
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
match self.get_mut() {
NotaryConnection::Tcp(stream) => Pin::new(stream).poll_shutdown(cx),
NotaryConnection::Tls(stream) => Pin::new(stream).poll_shutdown(cx),
}
}
}
/// Client that sets up connection to notary server.
#[derive(Debug, Clone, derive_builder::Builder)]
pub struct NotaryClient {
/// Host of the notary server endpoint, either a DNS name (if TLS is used)
/// or IP address.
#[builder(setter(into))]
host: String,
/// Port of the notary server endpoint.
#[builder(default = "self.default_port()")]
port: u16,
/// URL path prefix of the notary server endpoint, e.g. "https://<host>:<port>/<path_prefix>/...".
#[builder(setter(into), default = "String::from(\"\")")]
path_prefix: String,
/// Flag to turn on/off using TLS with notary server.
#[builder(setter(name = "enable_tls"), default = "true")]
tls: bool,
/// Root certificate store used for establishing TLS connection with notary
/// server.
#[builder(default = "default_root_store()")]
root_cert_store: RootCertStore,
/// API key used to call notary server endpoints if whitelisting is enabled
/// in notary server.
#[builder(setter(into, strip_option), default)]
api_key: Option<String>,
/// JWT token used to call notary server endpoints if JWT authorization is
/// enabled in notary server.
#[builder(setter(into, strip_option), default)]
jwt: Option<String>,
/// The duration of notarization request timeout in seconds.
#[builder(default = "60")]
request_timeout: usize,
/// The number of seconds to wait between notarization request retries.
///
/// By default uses the value suggested by the server.
#[builder(default = "None")]
request_retry_override: Option<u64>,
}
impl NotaryClientBuilder {
// Default setter of port.
fn default_port(&self) -> u16 {
// If port is not specified, set it to 80 if TLS is off, else 443 since TLS is
// on (including when self.tls = None, which means it's set to default
// (true)).
if let Some(false) = self.tls {
80
} else {
443
}
}
}
impl NotaryClient {
/// Creates a new builder for `NotaryClient`.
pub fn builder() -> NotaryClientBuilder {
NotaryClientBuilder::default()
}
/// Configures and requests a notarization, returning a connection to the
/// notary server if successful.
pub async fn request_notarization(
&self,
notarization_request: NotarizationRequest,
) -> Result<Accepted, ClientError> {
let notary_socket = tokio::net::TcpStream::connect((self.host.as_str(), self.port))
.await
.map_err(|err| ClientError::new(ErrorKind::Connection, Some(Box::new(err))))?;
// Setting TCP_NODELAY will improve prover latency.
let _ = notary_socket
.set_nodelay(true)
.map_err(|_| info!("An error occured when setting TCP_NODELAY. This will result in higher protocol latency."));
if self.tls {
debug!("Setting up tls connection...");
let notary_client_config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(self.root_cert_store.clone())
.with_no_client_auth();
let notary_connector = TlsConnector::from(Arc::new(notary_client_config));
let notary_tls_socket = notary_connector
.connect(
self.host.as_str().try_into().map_err(|err| {
error!("Failed to parse notary server DNS name: {:?}", self.host);
ClientError::new(ErrorKind::TlsSetup, Some(Box::new(err)))
})?,
notary_socket,
)
.await
.map_err(|err| {
if is_tls_mismatch_error(&err) {
error!("Perhaps the notary server is not accepting our TLS connection");
}
ClientError::new(ErrorKind::TlsSetup, Some(Box::new(err)))
})?;
self.send_request(notary_tls_socket, notarization_request)
.await
.map(|(connection, session_id)| Accepted {
id: session_id,
io: NotaryConnection::Tls(connection),
})
} else {
debug!("Setting up tcp connection...");
self.send_request(notary_socket, notarization_request)
.await
.map(|(connection, session_id)| Accepted {
id: session_id,
io: NotaryConnection::Tcp(connection),
})
}
}
/// Sends notarization request to the notary server.
async fn send_request<S: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
&self,
notary_socket: S,
notarization_request: NotarizationRequest,
) -> Result<(S, String), ClientError> {
let http_scheme = if self.tls { "https" } else { "http" };
let path_prefix = if self.path_prefix.is_empty() {
String::new()
} else {
format!("/{}", self.path_prefix)
};
// Attach the hyper HTTP client to the notary connection to send request to the
// /session endpoint to configure notarization and obtain session id.
let (mut notary_request_sender, notary_connection) =
hyper::client::conn::http1::handshake(TokioIo::new(notary_socket))
.await
.map_err(|err| {
error!("Failed to attach http client to notary socket");
ClientError::new(ErrorKind::Connection, Some(Box::new(err)))
})?;
// Create a future to poll the notary connection to completion before extracting
// the socket.
let notary_connection_fut = async {
// Claim back notary socket after HTTP exchange is done.
let Parts {
io: notary_socket, ..
} = notary_connection.without_shutdown().await.map_err(|err| {
error!("Failed to claim back notary socket after HTTP exchange is done");
ClientError::new(ErrorKind::Internal, Some(Box::new(err)))
})?;
Ok(notary_socket)
};
// Create a future to send configuration and notarization requests to the notary
// server using the connection established above.
let client_requests_fut = async {
// Build the HTTP request to configure notarization.
let configuration_request_payload =
serde_json::to_string(&NotarizationSessionRequest {
client_type: ClientType::Tcp,
plugin: notarization_request.plugin,
max_sent_data: Some(notarization_request.max_sent_data),
max_recv_data: Some(notarization_request.max_recv_data),
})
.map_err(|err| {
error!("Failed to serialise http request for configuration");
ClientError::new(ErrorKind::Internal, Some(Box::new(err)))
})?;
let mut configuration_request_builder = Request::builder()
.uri(format!(
"{http_scheme}://{}:{}{}/session",
self.host, self.port, path_prefix
))
.method("POST")
.header("Host", &self.host)
// Need to specify application/json for axum to parse it as json.
.header("Content-Type", "application/json");
if let Some(api_key) = &self.api_key {
configuration_request_builder =
configuration_request_builder.header(X_API_KEY_HEADER, api_key);
}
if let Some(jwt) = &self.jwt {
configuration_request_builder =
configuration_request_builder.header(AUTHORIZATION, format!("Bearer {jwt}"));
}
let configuration_request = configuration_request_builder
.body(Either::Left(Full::new(Bytes::from(
configuration_request_payload,
))))
.map_err(|err| {
error!("Failed to build http request for configuration");
ClientError::new(ErrorKind::Internal, Some(Box::new(err)))
})?;
debug!("Sending configuration request: {:?}", configuration_request);
let configuration_response = notary_request_sender
.send_request(configuration_request)
.await
.map_err(|err| {
error!("Failed to send http request for configuration");
ClientError::new(ErrorKind::Http, Some(Box::new(err)))
})?;
debug!("Sent configuration request");
if configuration_response.status() != StatusCode::OK {
return Err(ClientError::new(
ErrorKind::Configuration,
Some(
format!(
"Configuration response status is not OK: {:?}",
configuration_response
)
.into(),
),
));
}
let configuration_response_payload = configuration_response
.into_body()
.collect()
.await
.map_err(|err| {
error!("Failed to parse configuration response");
ClientError::new(ErrorKind::Http, Some(Box::new(err)))
})?
.to_bytes();
let configuration_response_payload_parsed =
serde_json::from_str::<NotarizationSessionResponse>(&String::from_utf8_lossy(
&configuration_response_payload,
))
.map_err(|err| {
error!("Failed to parse configuration response payload");
ClientError::new(ErrorKind::Internal, Some(Box::new(err)))
})?;
debug!(
"Configuration response: {:?}",
configuration_response_payload_parsed
);
// Send notarization request via HTTP, where the underlying TCP/TLS connection
// will be extracted later.
let notarization_request = Request::builder()
// 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.
.uri(format!(
"{http_scheme}://{}:{}{}/notarize?sessionId={}",
self.host,
self.port,
path_prefix,
&configuration_response_payload_parsed.session_id
))
.method("GET")
.header("Host", &self.host)
.header("Connection", "Upgrade")
// Need to specify this upgrade header for server to extract TCP/TLS connection
// later.
.header("Upgrade", "TCP")
.body(Either::Right(Empty::<Bytes>::new()))
.map_err(|err| {
error!("Failed to build http request for notarization");
ClientError::new(ErrorKind::Internal, Some(Box::new(err)))
})?;
debug!("Sending notarization request: {:?}", notarization_request);
let notarize_with_retry_fut = async {
loop {
let notarization_response = notary_request_sender
.send_request(notarization_request.clone())
.await
.map_err(|err| {
error!("Failed to send http request for notarization");
ClientError::new(ErrorKind::Http, Some(Box::new(err)))
})?;
if notarization_response.status() == StatusCode::SWITCHING_PROTOCOLS {
return Ok::<Response<Incoming>, ClientError>(notarization_response);
} else if notarization_response.status() == StatusCode::SERVICE_UNAVAILABLE {
let retry_after = self
.request_retry_override
.unwrap_or(parse_retry_after(&notarization_response)?);
debug!("Retrying notarization request in {:?}", retry_after);
sleep(Duration::from_secs(retry_after)).await;
} else {
return Err(ClientError::new(
ErrorKind::Internal,
Some(
format!(
"Server sent unexpected status code {:?}",
notarization_response.status()
)
.into(),
),
));
}
}
};
let notarization_response = timeout(
Duration::from_secs(self.request_timeout as u64),
notarize_with_retry_fut,
)
.await
.map_err(|_| {
ClientError::new(
ErrorKind::Internal,
Some(
"Timed out while waiting for server to accept notarization request".into(),
),
)
})??;
debug!("Notarization request was accepted by the server");
if notarization_response.status() != StatusCode::SWITCHING_PROTOCOLS {
return Err(ClientError::new(
ErrorKind::Internal,
Some(
format!(
"Notarization response status is not SWITCHING_PROTOCOL: {:?}",
notarization_response
)
.into(),
),
));
}
Ok(configuration_response_payload_parsed.session_id)
};
// Poll both futures simultaneously to obtain the resulting socket and
// session_id.
let (notary_socket, session_id) =
futures::try_join!(notary_connection_fut, client_requests_fut)?;
Ok((notary_socket.into_inner(), session_id))
}
/// Sets notarization request timeout duration in seconds.
pub fn request_timeout(&mut self, timeout: usize) {
self.request_timeout = timeout;
}
/// Sets the number of seconds to wait between notarization request
/// retries.
pub fn request_retry_override(&mut self, seconds: u64) {
self.request_retry_override = Some(seconds);
}
}
/// Default root store using mozilla certs.
fn default_root_store() -> RootCertStore {
let mut root_store = RootCertStore::empty();
root_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject.as_ref(),
ta.subject_public_key_info.as_ref(),
ta.name_constraints.as_ref().map(|nc| nc.as_ref()),
)
}));
root_store
}
// Checks whether the error is potentially related to a mismatch in TLS
// configuration between the client and the server.
fn is_tls_mismatch_error(err: &std::io::Error) -> bool {
if let Some(rustls::Error::InvalidMessage(rustls::InvalidMessage::InvalidContentType)) = err
.get_ref()
.and_then(|inner| inner.downcast_ref::<rustls::Error>())
{
return true;
}
false
}
// Attempts to parse the value of the "Retry-After" header from the given
// `response`.
fn parse_retry_after(response: &Response<Incoming>) -> Result<u64, ClientError> {
let seconds = match response.headers().get("Retry-After") {
Some(value) => {
let value_str = value.to_str().map_err(|err| {
ClientError::new(
ErrorKind::Internal,
Some(format!("Invalid Retry-After header: {}", err).into()),
)
})?;
let seconds: u64 = value_str.parse().map_err(|err| {
ClientError::new(
ErrorKind::Internal,
Some(format!("Could not parse Retry-After header as number: {}", err).into()),
)
})?;
seconds
}
None => {
return Err(ClientError::new(
ErrorKind::Internal,
Some("The expected Retry-After header was not found in server response".into()),
));
}
};
Ok(seconds)
}

View File

@@ -1,48 +0,0 @@
//! Notary client errors.
//!
//! This module handles errors that might occur during connection setup and
//! notarization requests.
use derive_builder::UninitializedFieldError;
use std::{error::Error, fmt};
#[derive(Debug)]
#[allow(missing_docs)]
pub(crate) enum ErrorKind {
Internal,
Builder,
Connection,
TlsSetup,
Http,
Configuration,
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub struct ClientError {
kind: ErrorKind,
#[source]
source: Option<Box<dyn Error + Send + Sync>>,
}
impl ClientError {
pub(crate) fn new(kind: ErrorKind, source: Option<Box<dyn Error + Send + Sync>>) -> Self {
Self { kind, source }
}
}
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"client error: {:?}, source: {:?}",
self.kind, self.source
)
}
}
impl From<UninitializedFieldError> for ClientError {
fn from(ufe: UninitializedFieldError) -> Self {
ClientError::new(ErrorKind::Builder, Some(Box::new(ufe)))
}
}

View File

@@ -1,15 +0,0 @@
//! Notary client library.
//!
//! A notary client's purpose is to establish a connection to the notary server
//! via TCP or TLS, and to configure and request notarization.
//! Note that the actual notarization is not performed by the notary client but
//! by the prover of the TLSNotary protocol.
#![deny(missing_docs, unreachable_pub, unused_must_use)]
#![deny(clippy::all)]
#![forbid(unsafe_code)]
mod client;
mod error;
pub use client::{Accepted, NotarizationRequest, NotaryClient, NotaryConnection};
pub use error::ClientError;

View File

@@ -1,11 +0,0 @@
[package]
name = "notary-common"
version = "0.1.0-alpha.12"
description = "Common code shared between notary-server and notary-client"
edition = "2021"
[lints]
workspace = true
[dependencies]
serde = { workspace = true, features = ["derive"] }

View File

@@ -1,36 +0,0 @@
use serde::{Deserialize, Serialize};
/// Custom HTTP header used for specifying a whitelisted API key.
pub const X_API_KEY_HEADER: &str = "X-API-Key";
/// Types of client that the prover is using.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ClientType {
/// Client that has access to the transport layer.
Tcp,
/// Client that cannot directly access the transport layer, e.g. browser
/// extension.
Websocket,
}
/// Request object of the /session API.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotarizationSessionRequest {
pub client_type: ClientType,
/// Name of verifier plugin that prover wants to interact with.
pub plugin: String,
/// Maximum data that can be sent by the prover.
pub max_sent_data: Option<usize>,
/// Maximum data that can be received by the prover.
pub max_recv_data: Option<usize>,
}
/// Response object of the /session API.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotarizationSessionResponse {
/// Unique session id that is generated by the notary and shared to the
/// prover.
pub session_id: String,
}

View File

@@ -1 +0,0 @@
wasm/

View File

@@ -1,46 +0,0 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.swp
pids
logs
results
tmp
# Build
public/css/main.css
# Coverage reports
coverage
# API keys and secrets
.env
# Dependency directory
node_modules
bower_components
# Editors
.idea
*.iml
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
dist/**/*
# ignore yarn.lock
yarn.lock
# include relevant json files
!package.json
!package-lock.json
!tsconfig.json

View File

@@ -1,4 +0,0 @@
node_modules
dist
.git

Some files were not shown because too many files have changed in this diff Show More