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
194 changed files with 3505 additions and 9864 deletions

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 }}"

2208
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

@@ -8,11 +8,8 @@ version = "0.0.0"
workspace = true
[dependencies]
notary-client = { 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 }
@@ -43,18 +40,6 @@ tokio-util = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
[[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"

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

@@ -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,532 +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 {
/// 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,
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,34 +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, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotarizationSessionRequest {
pub client_type: ClientType,
/// 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,67 +0,0 @@
[package]
name = "notary-server"
version = "0.1.0-alpha.12"
edition = "2021"
[lints]
workspace = true
[features]
tee_quote = ["dep:mc-sgx-dcap-types", "dep:hex"]
[dependencies]
notary-common = { workspace = true }
tlsn-core = { workspace = true }
tlsn-common = { workspace = true }
tlsn-verifier = { workspace = true }
async-tungstenite = { workspace = true, features = ["tokio-native-tls"] }
axum = { workspace = true, features = ["ws"] }
axum-core = { version = "0.5" }
axum-macros = { version = "0.5" }
base64 = { version = "0.21" }
config = { version = "0.14", features = ["yaml"] }
const-oid = { version = "0.9.6", features = ["db"] }
csv = { version = "1.3" }
eyre = { version = "0.6" }
futures-util = { workspace = true }
http = { workspace = true }
http-body-util = { workspace = true }
hyper = { workspace = true, features = ["client", "http1", "server"] }
hyper-util = { workspace = true, features = ["full"] }
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
k256 = { workspace = true }
notify = { version = "6.1.1", default-features = false, features = [
"macos_kqueue",
] }
p256 = { workspace = true }
pkcs8 = { workspace = true, features = ["pem"] }
rand = { workspace = true }
rand06-compat = { workspace = true }
rustls = { workspace = true }
rustls-pemfile = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = { version = "0.9" }
sha1 = { version = "0.10" }
structopt = { version = "0.3" }
strum = { version = "0.27", features = ["derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-rustls = { workspace = true }
tokio-util = { workspace = true, features = ["compat"] }
tower-http = { workspace = true, features = ["cors"] }
tower-service = { workspace = true }
tower-util = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
uuid = { workspace = true, features = ["v4", "fast-rng"] }
ws_stream_tungstenite = { workspace = true, features = ["tokio_io"] }
zeroize = { workspace = true }
hex = { workspace = true, optional = true }
mc-sgx-dcap-types = { version = "0.11.0", optional = true }
[build-dependencies]
git2 = "0.19.0"
chrono.workspace = true

View File

@@ -1,226 +0,0 @@
# notary-server
An implementation of the notary server in Rust.
## ⚠️ Notice
This crate is currently under active development and should not be used in production. Expect bugs and regular major breaking changes.
---
## Running the server
### ⚠️ Notice
- When running this server against a prover (e.g. [Rust](../../examples/) or [browser extension](https://github.com/tlsnotary/tlsn-extension)), please ensure that the prover's version is the same as the version of this server.
- When running this server in a *production environment*, please first read this [page](https://docs.tlsnotary.org/developers/notary_server.html).
### Using Cargo
Start the server with:
```bash
cargo run --release --bin notary-server
```
### Using Docker
There are two ways to obtain the notary server's Docker image.
- [GitHub](#obtaining-the-image-via-github)
- [Building from source](#building-from-source)
#### GitHub
1. Obtain the latest image.
```bash
docker pull ghcr.io/tlsnotary/tlsn/notary-server:latest
```
2. Run the docker container.
```bash
docker run --init -p 127.0.0.1:7047:7047 ghcr.io/tlsnotary/tlsn/notary-server:latest
```
#### Building from source
1. Build the docker image at the root of this *repository*.
```bash
docker build . -t notary-server:local -f crates/notary/server/notary-server.Dockerfile
```
2. Run the docker container.
```bash
docker run --init -p 127.0.0.1:7047:7047 notary-server:local
```
---
## Configuration
### Default
Refer to [config.rs](./src/config.rs) for more information on the definition of these setting parameters.
```yaml
host: "0.0.0.0"
port: 7047
html_info: |
<head>
<meta charset="UTF-8">
<meta name="author" content="tlsnotary">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<svg width="86" height="88" viewBox="0 0 86 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5484 0.708986C25.5484 0.17436 26.1196 -0.167376 26.5923 0.0844205L33.6891 3.86446C33.9202 3.98756 34.0645 4.22766 34.0645 4.48902V9.44049H37.6129C38.0048 9.44049 38.3226 9.75747 38.3226 10.1485V21.4766L36.1936 20.0606V11.5645H34.0645V80.9919C34.0645 81.1134 34.0332 81.2328 33.9735 81.3388L30.4251 87.6388C30.1539 88.1204 29.459 88.1204 29.1878 87.6388L25.6394 81.3388C25.5797 81.2328 25.5484 81.1134 25.5484 80.9919V0.708986Z" fill="#243F5F"/>
<path d="M21.2903 25.7246V76.7012H12.7742V34.2207H0V25.7246H21.2903Z" fill="#243F5F"/>
<path d="M63.871 76.7012H72.3871V34.2207H76.6452V76.7012H85.1613V25.7246H63.871V76.7012Z" fill="#243F5F"/>
<path d="M38.3226 25.7246H59.6129V34.2207H46.8387V46.9649H59.6129V76.7012H38.3226V68.2051H51.0968V55.4609H38.3226V25.7246Z" fill="#243F5F"/>
</svg>
<h1>Notary Server {version}!</h1>
<ul>
<li>public key: <pre>{public_key}</pre></li>
<li>git commit hash: <a href="https://github.com/tlsnotary/tlsn/commit/{git_commit_hash}">{git_commit_hash}</a></li>
<li><a href="healthcheck">health check</a></li>
<li><a href="info">info</a></li>
</ul>
</body>
concurrency: 32
notarization:
max_sent_data: 4096
max_recv_data: 16384
timeout: 1800
private_key_path: null
signature_algorithm: secp256k1
tls:
enabled: false
private_key_path: null
certificate_path: null
log:
level: DEBUG
filter: null
format: COMPACT
auth:
enabled: false
whitelist: null
```
⚠️ By default, `notarization.private_key_path` is `null`, which means a **random, ephemeral** signing key will be generated at runtime (see [Signing](#signing) for more details).
### Overriding default
The default setting can be overriden with either (1) environment variables, or (2) a configuration file (yaml).
#### Environment Variables
Default values can be overriden by setting environment variables. The variables have a `NS_`-prefix followed by the configuration key in uppercase. Double underscores are used for nested configuration keys, e.g. `tls.enabled` will be `NS_TLS__ENABLED`.
Example:
```bash
NS_PORT=8080 NS_NOTARIZATION__MAX_SENT_DATA=2048 cargo run --release --bin notary-server
```
#### Configuration File
This will override all the default values, hence it needs to **contain all compulsory** configuration keys and values (refer to the [default yaml](#default)). The config file has precedence over environment variables.
```bash
cargo run --release --bin notary-server -- --config <path to your config.yaml>
```
### When using Docker
1. Override the port.
```bash
docker run --init -p 127.0.0.1:7070:7070 -e NS_PORT=7070 notary-server:local
```
2. Override the notarization private key path, and map a local private key into the container.
```bash
docker run --init -p 127.0.0.1:7047:7047 -e NS_NOTARIZATION__PRIVATE_KEY_PATH="/root/.notary/notary.key" -v <your private key>:/root/.notary/notary.key notary-server:local
```
3. Override with a configuration file.
```bash
docker run --init -p 127.0.0.1:7047:7047 -v <your config.yaml>:/root/.notary/config.yaml notary-server:local --config /root/.notary/config.yaml
```
⚠️ The default `workdir` of the container is `/root/.notary`.
---
## API
### HTTP APIs
Defined in the [OpenAPI specification](./openapi.yaml).
### WebSocket APIs
#### /notarize
##### Description
To perform a notarization using a session id — an unique id returned upon calling the `/session` endpoint successfully.
##### Query Parameter
`sessionId`
##### Query Parameter Type
String
---
## Features
### Notarization Configuration
To perform a notarization, some parameters need to be configured by the prover and the notary server (more details in the [OpenAPI specification](./openapi.yaml)), i.e.
- maximum data that can be sent and received.
- unique session id.
To streamline this process, a single HTTP endpoint (`/session`) is used by both TCP and WebSocket clients.
### Notarization
After calling the configuration endpoint above, the prover can proceed to start the notarization. For a TCP client, that means calling the `/notarize` endpoint using HTTP, while a WebSocket client should call the same endpoint but using WebSocket. Example implementations of these clients can be found in the [integration test](../tests-integration/tests/notary.rs).
### Signing
To sign the notarized transcript, the notary server requires a signing key. If this signing key (`notarization.private_key_path` in the config) is not provided by the user, then **by default, a random, ephemeral** signing key will be generated at runtime.
This ephemeral key, along with its public key, are not persisted. The keys disappear once the server stops. This makes the keys only suitable for testing.
### TLS
TLS needs to be turned on between the prover and the notary for security purposes. It can be turned off though, if any of the following is true.
1. This server is run locally.
2. TLS is to be handled by an external environment, e.g. reverse proxy, cloud setup.
The toggle to turn on TLS, as well as paths to the TLS private key and certificate can be defined in the config (`tls` field).
### Authorization
An optional authorization module is available to only allow requests with a valid credential attached. Currently, two modes are supported: whitelist and JWT.
Please note that only *one* mode can be active at any one time.
#### Whitelist mode
In whitelist mode, a valid API key needs to be attached in the custom HTTP header `X-API-Key`. The path of the API key whitelist, as well as the flag to enable/disable this module, can be changed in the config (`auth` field).
Hot reloading of the whitelist is supported, i.e. changes to the whitelist file are automatically applied without needing to restart the server.
#### JWT mode
In JWT mode, JSON Web Token is attached in the standard `Authorization` HTTP header as a bearer token. The algorithm, the path to verifying key, as well as custom user claims, can be changed in the config (`auth` field).
Care should be taken when defining custom user claims as the middleware will:
- accept any claim if no custom claim is defined,
- as long as user defined claims are found, other unknown claims will be ignored.
An example JWT config may look something like this:
```yaml
auth:
enabled: true
jwt:
algorithm: "RS256"
public_key_path: "./fixture/auth/jwt.key.pub"
claims:
- name: sub
values: ["tlsnotary"]
```
### Logging
The default logging strategy of this server is set to `DEBUG` verbosity level for the crates that are useful for most debugging scenarios, i.e. using the following filtering logic.
`notary_server=DEBUG,tlsn_verifier=DEBUG,mpc_tls=DEBUG,tls_client_async=DEBUG`
In the configuration, one can toggle the verbosity level for these crates using the `level` field under `logging`.
One can also provide a custom filtering logic by adding a `filter` field under `logging`, and use a value that follows the tracing crate's [filter directive syntax](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax).
Logs can be printed in two formats. Compact and JSON. Compact is human-readable and is best suited for console. JSON is machine-readable and is used to send logs to log collection services. One can change log format by switching the `format` field under `logging`. Accepted values are `COMPACT` and `JSON`. `COMPACT` is used by default.
### Concurrency
One can limit the number of concurrent notarization requests from provers via `concurrency` in the config. This is to limit resource utilization and mitigate potential DoS attacks.
---
## Architecture
### Objective
The main objective of a notary server is to perform notarizations together with a prover. In this case, the prover can either be a
1. TCP client — which has access and control over the transport layer, i.e. TCP.
2. WebSocket client — which has no access over TCP and instead uses WebSocket for notarizations.
### Design Choices
#### Web Framework
Axum is chosen as the framework to serve HTTP and WebSocket requests from the prover clients due to its rich and well supported features, e.g. native integration with Tokio/Hyper/Tower, customizable middleware, the ability to support lower level integrations of TLS ([example](https://github.com/tokio-rs/axum/blob/main/examples/low-level-rustls/src/main.rs)). To simplify the notary server setup, a single Axum router is used to support both HTTP and WebSocket connections, i.e. all requests can be made to the same port of the notary server.
#### WebSocket
Axum's internal implementation of WebSocket uses [tokio_tungstenite](https://docs.rs/tokio-tungstenite/latest/tokio_tungstenite/), which provides a WebSocket struct that doesn't implement [AsyncRead](https://docs.rs/futures/latest/futures/io/trait.AsyncRead.html) and [AsyncWrite](https://docs.rs/futures/latest/futures/io/trait.AsyncWrite.html). Both these traits are required by the TLSN core libraries for the prover and the notary. To overcome this, a [slight modification](./src/service/axum_websocket.rs) of Axum's implementation of WebSocket is used, where [async_tungstenite](https://docs.rs/async-tungstenite/latest/async_tungstenite/) is used instead so that [ws_stream_tungstenite](https://docs.rs/ws_stream_tungstenite/latest/ws_stream_tungstenite/index.html) can be used to wrap on top of the WebSocket struct to get AsyncRead and AsyncWrite implemented.

View File

@@ -1,55 +0,0 @@
use chrono::DateTime;
use git2::{Commit, Repository, StatusOptions};
use std::{env, error::Error};
fn main() -> Result<(), Box<dyn Error>> {
if env::var("GIT_COMMIT_HASH").is_err() {
match get_commithash_with_dirty_suffix() {
Ok(commit_hash_with_suffix) => {
// Pass value as env var to the notary server
println!("cargo:rustc-env=GIT_COMMIT_HASH={commit_hash_with_suffix}");
}
Err(e) => {
eprintln!("Failed to get commit hash in notary server build");
eprintln!("Fix the error or configure GIT_COMMIT_HASH as environment variable");
return Err(e.message().into());
}
};
}
Ok(())
}
fn get_commithash_with_dirty_suffix() -> Result<String, git2::Error> {
let repo = Repository::discover(".")?;
let commit = get_commit(&repo)?;
let commit_hash = commit.id().to_string();
let _timestamp = get_commit_timestamp(&commit)?;
let has_changes = check_local_changes(&repo)?;
if has_changes {
Ok(format!("{commit_hash} (with local changes)"))
} else {
Ok(commit_hash)
}
}
fn get_commit(repo: &Repository) -> Result<Commit, git2::Error> {
let head = repo.head()?;
head.peel_to_commit()
}
fn get_commit_timestamp(commit: &Commit) -> Result<String, git2::Error> {
let timestamp = commit.time().seconds();
let date_time = DateTime::from_timestamp(timestamp, 0)
.ok_or_else(|| git2::Error::from_str("Invalid timestamp"))?;
Ok(date_time.to_rfc2822())
}
fn check_local_changes(repo: &Repository) -> Result<bool, git2::Error> {
let mut status_options = StatusOptions::new();
status_options
.include_untracked(false)
.include_ignored(false);
let statuses = repo.statuses(Some(&mut status_options))?;
Ok(!statuses.is_empty())
}

View File

@@ -1,19 +0,0 @@
# !!! To use this file, please run docker run at the root level of this repository
FROM rust:latest AS builder
RUN apt-get update && apt-get install -y clang libclang-dev
WORKDIR /usr/src/tlsn
COPY . .
RUN cargo install --locked --path crates/notary/server
FROM ubuntu:latest
WORKDIR /root/.notary
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/notary-server /usr/local/bin/notary-server
# Label to link this image with the repository in Github Container Registry (https://docs.github.com/en/packages/learn-github-packages/connecting-a-repository-to-a-package#connecting-a-repository-to-a-container-image-using-the-command-line)
LABEL org.opencontainers.image.source=https://github.com/tlsnotary/tlsn
LABEL org.opencontainers.image.description="An implementation of the notary server in Rust."
ENTRYPOINT [ "notary-server" ]

View File

@@ -1,4 +0,0 @@
# exclude Rust build artifacts
./target
./crates/wasm/pkg/
./crates/harness/static/generated/

View File

@@ -1,223 +0,0 @@
openapi: 3.0.0
info:
title: Notary Server
description: Notary server written in Rust to provide notarization service.
version: 0.1.0-alpha.12
tags:
- name: General
- name: Notarization
paths:
/healthcheck:
get:
tags:
- General
description: Healthcheck endpoint
security:
- {} # make security optional
- ApiKeyAuth: []
- BearerAuth: []
responses:
'200':
description: Ok response from server
content:
text/plain:
schema:
type: string
example: Ok
'401':
description: API key is invalid
content:
text/plain:
schema:
type: string
example: 'Unauthorized request from prover: Invalid API key.'
/info:
get:
tags:
- General
description: General information about the notary server
security:
- {} # make security optional
- ApiKeyAuth: []
- BearerAuth: []
responses:
'200':
description: Info response from server
content:
application/json:
schema:
$ref: '#/components/schemas/InfoResponse'
'401':
description: API key is invalid
content:
text/plain:
schema:
type: string
example: 'Unauthorized request from prover: Invalid API key.'
/session:
post:
tags:
- Notarization
description: Initialize and configure notarization for both TCP and WebSocket clients
security:
- {} # make security optional
- ApiKeyAuth: []
- BearerAuth: []
parameters:
- in: header
name: Content-Type
description: The value must be application/json
schema:
type: string
enum:
- application/json
required: true
requestBody:
description: Notarization session request to server
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NotarizationSessionRequest'
responses:
'200':
description: Notarization session response from server
content:
application/json:
schema:
$ref: '#/components/schemas/NotarizationSessionResponse'
'400':
description: Configuration parameters or headers provided by prover are invalid
content:
text/plain:
schema:
type: string
example: 'Invalid request from prover: Failed to deserialize the JSON body into the target type'
'401':
description: API key is invalid
content:
text/plain:
schema:
type: string
example: 'Unauthorized request from prover: Invalid API key.'
'500':
description: There was some internal error when processing
content:
text/plain:
schema:
type: string
example: Something is wrong
/notarize:
get:
tags:
- Notarization
description: Start notarization for TCP client
parameters:
- in: header
name: Connection
description: The value should be 'Upgrade'
schema:
type: string
enum:
- Upgrade
required: true
- in: header
name: Upgrade
description: The value should be 'TCP'
schema:
type: string
enum:
- TCP
required: true
- in: query
name: sessionId
description: Unique ID returned from server upon calling POST /session
schema:
type: string
required: true
responses:
'101':
description: Switching protocol response
'400':
description: Headers provided by prover are invalid
content:
text/plain:
schema:
type: string
example: 'Invalid request from prover: Upgrade header is not set for client'
'500':
description: There was some internal error when processing
content:
text/plain:
schema:
type: string
example: Something is wrong
components:
schemas:
NotarizationSessionRequest:
type: object
properties:
clientType:
description: Types of client that the prover is using
type: string
enum:
- Tcp
- Websocket
maxSentData:
description: Maximum data that can be sent by the prover in bytes
type: integer
maxRecvData:
description: Maximum data that can be received by the prover in bytes
type: integer
required:
- clientType
NotarizationSessionResponse:
type: object
properties:
sessionId:
description: Unique ID returned from server upon calling POST /session
type: string
required:
- sessionId
InfoResponse:
type: object
properties:
version:
description: Current version of notary server
type: string
publicKey:
description: Public key of notary server for its notarization transcript signature
type: string
gitCommitHash:
description: The git commit hash of source code that this notary server is running
type: string
quote:
type: object
properties:
rawQuote:
description: Hex bytes representing the signed-by-intel quote
type: string
mrsigner:
description: Represents the public key of the enclave signer
type: string
mrenclave:
description: The enclave image hash, including gramine and the notary server itself
type: string
error:
description: Error that occurs when generating this quote
type: string
required:
- version
- publicKey
- gitCommitHash
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: Whitelisted API key if auth module is turned on and in whitelist mode
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JSON Web Token if auth module is turned on and in JWT mode

View File

@@ -1,81 +0,0 @@
pub(crate) mod jwt;
pub(crate) mod whitelist;
use eyre::{eyre, Result};
use jwt::load_jwt_key;
use std::{
str::FromStr,
sync::{Arc, Mutex},
};
use strum::VariantNames;
use tracing::debug;
use whitelist::load_authorization_whitelist;
pub use jwt::{Algorithm, Jwt};
pub use whitelist::{
watch_and_reload_authorization_whitelist, AuthorizationWhitelistRecord, Whitelist,
};
use crate::{AuthorizationModeProperties, NotaryServerProperties};
/// Supported authorization modes.
#[derive(Clone)]
pub enum AuthorizationMode {
Jwt(Jwt),
Whitelist(Whitelist),
}
impl AuthorizationMode {
pub fn as_whitelist(&self) -> Option<&Whitelist> {
match self {
Self::Jwt(..) => None,
Self::Whitelist(whitelist) => Some(whitelist),
}
}
}
/// Load authorization mode if it is enabled
pub async fn load_authorization_mode(
config: &NotaryServerProperties,
) -> Result<Option<AuthorizationMode>> {
if !config.auth.enabled {
debug!("Skipping authorization as it is turned off.");
return Ok(None);
}
let auth_mode = match config.auth.mode.as_ref().ok_or_else(|| {
eyre!(
"Authorization enabled but failed to load either whitelist or jwt properties. They are either absent or malformed."
)
})? {
AuthorizationModeProperties::Jwt(jwt_opts) => {
debug!("Using JWT for authorization");
let algorithm = Algorithm::from_str(&jwt_opts.algorithm).map_err(|_| {
eyre!(
"Unexpected JWT signing algorithm specified: '{}'. Possible values are: {:?}",
jwt_opts.algorithm,
Algorithm::VARIANTS,
)
})?;
let claims = jwt_opts.claims.clone();
let key = load_jwt_key(&jwt_opts.public_key_path, algorithm)
.await
.map_err(|err| eyre!("Failed to parse JWT public key: {:?}", err))?;
AuthorizationMode::Jwt(Jwt {
key,
claims,
algorithm,
})
}
AuthorizationModeProperties::Whitelist(whitelist_csv_path) => {
debug!("Using whitelist for authorization");
let entries = load_authorization_whitelist(whitelist_csv_path)?;
AuthorizationMode::Whitelist(Whitelist {
entries: Arc::new(Mutex::new(entries)),
csv_path: whitelist_csv_path.clone(),
})
}
};
Ok(Some(auth_mode))
}

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