mirror of
https://github.com/tlsnotary/tlsn.git
synced 2026-01-10 22:08:09 -05:00
Compare commits
31 Commits
poc/wasmti
...
blake3_exa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6c5c13aa | ||
|
|
4ab73bdfb5 | ||
|
|
a8fa57f2cb | ||
|
|
d25fb320d4 | ||
|
|
0539268da7 | ||
|
|
427b2896b5 | ||
|
|
89d1e594d1 | ||
|
|
b4380f021e | ||
|
|
8a823d18ec | ||
|
|
7bcfc56bd8 | ||
|
|
2909d5ebaa | ||
|
|
7918494ccc | ||
|
|
92dd47b376 | ||
|
|
5474a748ce | ||
|
|
92da5adc24 | ||
|
|
e0ce1ad31a | ||
|
|
3b76877920 | ||
|
|
783355772a | ||
|
|
e5c59da90b | ||
|
|
f059c53c2d | ||
|
|
a1367b5428 | ||
|
|
9d8124ac9d | ||
|
|
5034366c72 | ||
|
|
afd8f44261 | ||
|
|
21086d2883 | ||
|
|
cca9a318a4 | ||
|
|
cb804a6025 | ||
|
|
9f849e7c18 | ||
|
|
389bceddef | ||
|
|
657838671a | ||
|
|
2f072b2578 |
207
.github/workflows/ci.yml
vendored
207
.github/workflows/ci.yml
vendored
@@ -11,7 +11,6 @@ on:
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
attestations: write
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -19,11 +18,10 @@ env:
|
||||
# We need a higher number of parallel rayon tasks than the default (which is 4)
|
||||
# in order to prevent a deadlock, c.f.
|
||||
# - https://github.com/tlsnotary/tlsn/issues/548
|
||||
# - https://github.com/privacy-scaling-explorations/mpz/issues/178
|
||||
# - https://github.com/privacy-ethereum/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.88.0
|
||||
RUST_VERSION: 1.90.0
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
@@ -34,7 +32,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
components: clippy
|
||||
@@ -159,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
|
||||
|
||||
@@ -186,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
|
||||
@@ -395,4 +198,4 @@ jobs:
|
||||
draft: true
|
||||
tag_name: ${{ github.ref_name }}
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: true
|
||||
|
||||
1
.github/workflows/rustdoc.yml
vendored
1
.github/workflows/rustdoc.yml
vendored
@@ -23,7 +23,6 @@ jobs:
|
||||
- name: "rustdoc"
|
||||
run: crates/wasm/build-docs.sh
|
||||
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
|
||||
3737
Cargo.lock
generated
3737
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
55
Cargo.toml
55
Cargo.toml
@@ -9,10 +9,6 @@ members = [
|
||||
"crates/data-fixtures",
|
||||
"crates/examples",
|
||||
"crates/formats",
|
||||
"crates/notary/client",
|
||||
"crates/notary/common",
|
||||
"crates/notary/server",
|
||||
"crates/notary/tests-integration",
|
||||
"crates/server-fixture/certs",
|
||||
"crates/server-fixture/server",
|
||||
"crates/tls/backend",
|
||||
@@ -43,11 +39,10 @@ opt-level = 1
|
||||
[profile.wasm]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
|
||||
[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" }
|
||||
@@ -71,19 +66,19 @@ tlsn-harness-runner = { path = "crates/harness/runner" }
|
||||
tlsn-wasm = { path = "crates/wasm" }
|
||||
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" }
|
||||
mpz-common = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-vm-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-garble = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-garble-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-ole = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-ot = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-share-conversion = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-fields = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-zk = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-hash = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "ccc0057" }
|
||||
mpz-circuits = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-memory-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-common = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-vm-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-garble = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-garble-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-ole = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-ot = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-share-conversion = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-fields = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-zk = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-hash = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
|
||||
rangeset = { version = "0.2" }
|
||||
serio = { version = "0.2" }
|
||||
@@ -91,11 +86,11 @@ spansy = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6168663" }
|
||||
uid-mux = { version = "0.2" }
|
||||
websocket-relay = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6168663" }
|
||||
|
||||
aead = { version = "0.4" }
|
||||
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" }
|
||||
@@ -115,13 +110,11 @@ elliptic-curve = { version = "0.13" }
|
||||
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" }
|
||||
futures-rustls = { version = "0.25" }
|
||||
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" }
|
||||
@@ -134,7 +127,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" }
|
||||
@@ -148,6 +140,8 @@ rs_merkle = { git = "https://github.com/tlsnotary/rs-merkle.git", rev = "85f3e82
|
||||
rstest = { version = "0.17" }
|
||||
rustls = { version = "0.21" }
|
||||
rustls-pemfile = { version = "1.0" }
|
||||
rustls-webpki = { version = "0.103" }
|
||||
rustls-pki-types = { version = "1.12" }
|
||||
sct = { version = "0.7" }
|
||||
semver = { version = "1.0" }
|
||||
serde = { version = "1.0" }
|
||||
@@ -157,23 +151,18 @@ 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" }
|
||||
webpki-roots = { version = "1.0" }
|
||||
webpki-root-certs = { version = "1.0" }
|
||||
ws_stream_wasm = { version = "0.7.5" }
|
||||
zeroize = { version = "1.8" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,12 +21,11 @@ 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 }
|
||||
alloy-primitives = { version = "1.3.1", default-features = false }
|
||||
alloy-signer = { version = "1.0", default-features = false }
|
||||
alloy-signer-local = { version = "1.0", default-features = false }
|
||||
rand06-compat = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
tlsn-core = { workspace = true, features = ["fixtures"] }
|
||||
|
||||
@@ -242,7 +242,7 @@ impl std::fmt::Display for AttestationBuilderError {
|
||||
mod test {
|
||||
use rstest::{fixture, rstest};
|
||||
use tlsn_core::{
|
||||
connection::{HandshakeData, HandshakeDataV1_2},
|
||||
connection::{CertBinding, CertBindingV1_2},
|
||||
fixtures::{ConnectionFixture, encoding_provider},
|
||||
hash::Blake3,
|
||||
transcript::Transcript,
|
||||
@@ -399,10 +399,10 @@ mod test {
|
||||
server_cert_data, ..
|
||||
} = connection;
|
||||
|
||||
let HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let CertBinding::V1_2(CertBindingV1_2 {
|
||||
server_ephemeral_key,
|
||||
..
|
||||
}) = server_cert_data.handshake
|
||||
}) = server_cert_data.binding
|
||||
else {
|
||||
panic!("expected v1.2 handshake data");
|
||||
};
|
||||
@@ -470,10 +470,10 @@ mod test {
|
||||
..
|
||||
} = connection;
|
||||
|
||||
let HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let CertBinding::V1_2(CertBindingV1_2 {
|
||||
server_ephemeral_key,
|
||||
..
|
||||
}) = server_cert_data.handshake
|
||||
}) = server_cert_data.binding
|
||||
else {
|
||||
panic!("expected v1.2 handshake data");
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use tlsn_core::{
|
||||
connection::{CertificateVerificationError, ServerCertData, ServerEphemKey, ServerName},
|
||||
connection::{HandshakeData, HandshakeVerificationError, ServerEphemKey, ServerName},
|
||||
hash::{Blinded, HashAlgorithm, HashProviderError, TypedHash},
|
||||
};
|
||||
|
||||
@@ -30,14 +30,14 @@ use crate::{CryptoProvider, hash::HashAlgorithmExt, serialize::impl_domain_separ
|
||||
|
||||
/// Opens a [`ServerCertCommitment`].
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ServerCertOpening(Blinded<ServerCertData>);
|
||||
pub struct ServerCertOpening(Blinded<HandshakeData>);
|
||||
|
||||
impl_domain_separator!(ServerCertOpening);
|
||||
|
||||
opaque_debug::implement!(ServerCertOpening);
|
||||
|
||||
impl ServerCertOpening {
|
||||
pub(crate) fn new(data: ServerCertData) -> Self {
|
||||
pub(crate) fn new(data: HandshakeData) -> Self {
|
||||
Self(Blinded::new(data))
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl ServerCertOpening {
|
||||
}
|
||||
|
||||
/// Returns the server identity data.
|
||||
pub fn data(&self) -> &ServerCertData {
|
||||
pub fn data(&self) -> &HandshakeData {
|
||||
self.0.data()
|
||||
}
|
||||
}
|
||||
@@ -122,8 +122,8 @@ impl From<HashProviderError> for ServerIdentityProofError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CertificateVerificationError> for ServerIdentityProofError {
|
||||
fn from(err: CertificateVerificationError) -> Self {
|
||||
impl From<HandshakeVerificationError> for ServerIdentityProofError {
|
||||
fn from(err: HandshakeVerificationError) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::Certificate,
|
||||
message: err.to_string(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Attestation fixtures.
|
||||
|
||||
use tlsn_core::{
|
||||
connection::{HandshakeData, HandshakeDataV1_2},
|
||||
connection::{CertBinding, CertBindingV1_2},
|
||||
fixtures::ConnectionFixture,
|
||||
hash::HashAlgorithm,
|
||||
transcript::{
|
||||
@@ -16,11 +16,6 @@ use crate::{
|
||||
signing::SignatureAlgId,
|
||||
};
|
||||
|
||||
/// Returns a notary signing key fixture.
|
||||
pub fn notary_signing_key() -> p256::ecdsa::SigningKey {
|
||||
p256::ecdsa::SigningKey::from_slice(&[1; 32]).unwrap()
|
||||
}
|
||||
|
||||
/// A Request fixture used for testing.
|
||||
#[allow(missing_docs)]
|
||||
pub struct RequestFixture {
|
||||
@@ -72,7 +67,7 @@ pub fn request_fixture(
|
||||
let mut request_builder = Request::builder(&request_config);
|
||||
request_builder
|
||||
.server_name(server_name)
|
||||
.server_cert_data(server_cert_data)
|
||||
.handshake_data(server_cert_data)
|
||||
.transcript(transcript);
|
||||
|
||||
let (request, _) = request_builder.build(&provider).unwrap();
|
||||
@@ -96,12 +91,12 @@ pub fn attestation_fixture(
|
||||
..
|
||||
} = connection;
|
||||
|
||||
let HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let CertBinding::V1_2(CertBindingV1_2 {
|
||||
server_ephemeral_key,
|
||||
..
|
||||
}) = server_cert_data.handshake
|
||||
}) = server_cert_data.binding
|
||||
else {
|
||||
panic!("expected v1.2 handshake data");
|
||||
panic!("expected v1.2 binding data");
|
||||
};
|
||||
|
||||
let mut provider = CryptoProvider::default();
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use tls_core::{
|
||||
anchors::{OwnedTrustAnchor, RootCertStore},
|
||||
verify::WebPkiVerifier,
|
||||
};
|
||||
use tlsn_core::hash::HashProvider;
|
||||
use tlsn_core::{hash::HashProvider, webpki::ServerCertVerifier};
|
||||
|
||||
use crate::signing::{SignatureVerifierProvider, SignerProvider};
|
||||
|
||||
@@ -28,7 +24,7 @@ pub struct CryptoProvider {
|
||||
/// This is used to verify the server's certificate chain.
|
||||
///
|
||||
/// The default verifier uses the Mozilla root certificates.
|
||||
pub cert: WebPkiVerifier,
|
||||
pub cert: ServerCertVerifier,
|
||||
/// Signer provider.
|
||||
///
|
||||
/// This is used for signing attestations.
|
||||
@@ -45,21 +41,9 @@ impl Default for CryptoProvider {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hash: Default::default(),
|
||||
cert: default_cert_verifier(),
|
||||
cert: ServerCertVerifier::mozilla(),
|
||||
signer: Default::default(),
|
||||
signature: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn default_cert_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()),
|
||||
)
|
||||
}));
|
||||
WebPkiVerifier::new(root_store, None)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct Request {
|
||||
|
||||
impl Request {
|
||||
/// Returns a new request builder.
|
||||
pub fn builder(config: &RequestConfig) -> RequestBuilder {
|
||||
pub fn builder(config: &RequestConfig) -> RequestBuilder<'_> {
|
||||
RequestBuilder::new(config)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use tlsn_core::{
|
||||
connection::{ServerCertData, ServerName},
|
||||
connection::{HandshakeData, ServerName},
|
||||
transcript::{Transcript, TranscriptCommitment, TranscriptSecret},
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
pub struct RequestBuilder<'a> {
|
||||
config: &'a RequestConfig,
|
||||
server_name: Option<ServerName>,
|
||||
server_cert_data: Option<ServerCertData>,
|
||||
handshake_data: Option<HandshakeData>,
|
||||
transcript: Option<Transcript>,
|
||||
transcript_commitments: Vec<TranscriptCommitment>,
|
||||
transcript_commitment_secrets: Vec<TranscriptSecret>,
|
||||
@@ -25,7 +25,7 @@ impl<'a> RequestBuilder<'a> {
|
||||
Self {
|
||||
config,
|
||||
server_name: None,
|
||||
server_cert_data: None,
|
||||
handshake_data: None,
|
||||
transcript: None,
|
||||
transcript_commitments: Vec::new(),
|
||||
transcript_commitment_secrets: Vec::new(),
|
||||
@@ -38,9 +38,9 @@ impl<'a> RequestBuilder<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the server identity data.
|
||||
pub fn server_cert_data(&mut self, data: ServerCertData) -> &mut Self {
|
||||
self.server_cert_data = Some(data);
|
||||
/// Sets the handshake data.
|
||||
pub fn handshake_data(&mut self, data: HandshakeData) -> &mut Self {
|
||||
self.handshake_data = Some(data);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ impl<'a> RequestBuilder<'a> {
|
||||
let Self {
|
||||
config,
|
||||
server_name,
|
||||
server_cert_data,
|
||||
handshake_data: server_cert_data,
|
||||
transcript,
|
||||
transcript_commitments,
|
||||
transcript_commitment_secrets,
|
||||
|
||||
@@ -46,7 +46,7 @@ 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::connection::CertBinding);
|
||||
impl_domain_separator!(tlsn_core::transcript::TranscriptCommitment);
|
||||
impl_domain_separator!(tlsn_core::transcript::TranscriptSecret);
|
||||
impl_domain_separator!(tlsn_core::transcript::encoding::EncodingCommitment);
|
||||
|
||||
@@ -5,7 +5,7 @@ use tlsn_attestation::{
|
||||
signing::SignatureAlgId,
|
||||
};
|
||||
use tlsn_core::{
|
||||
connection::{HandshakeData, HandshakeDataV1_2},
|
||||
connection::{CertBinding, CertBindingV1_2},
|
||||
fixtures::{self, ConnectionFixture, encoder_secret},
|
||||
hash::Blake3,
|
||||
transcript::{
|
||||
@@ -36,10 +36,10 @@ fn test_api() {
|
||||
server_cert_data,
|
||||
} = ConnectionFixture::tlsnotary(transcript.length());
|
||||
|
||||
let HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let CertBinding::V1_2(CertBindingV1_2 {
|
||||
server_ephemeral_key,
|
||||
..
|
||||
}) = server_cert_data.handshake.clone()
|
||||
}) = server_cert_data.binding.clone()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -72,7 +72,7 @@ fn test_api() {
|
||||
|
||||
request_builder
|
||||
.server_name(server_name.clone())
|
||||
.server_cert_data(server_cert_data)
|
||||
.handshake_data(server_cert_data)
|
||||
.transcript(transcript)
|
||||
.transcript_commitments(
|
||||
vec![TranscriptSecret::Encoding(encoding_tree)],
|
||||
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
memory::{binary::U8, correlated::Delta, Array},
|
||||
prelude::*,
|
||||
};
|
||||
use mpz_zk::{Prover, Verifier};
|
||||
use mpz_zk::{Prover, ProverConfig, Verifier, VerifierConfig};
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
|
||||
use super::*;
|
||||
@@ -408,8 +408,8 @@ mod tests {
|
||||
|
||||
let gb = Garbler::new(cot_send, [0u8; 16], delta_mpc);
|
||||
let ev = Evaluator::new(cot_recv);
|
||||
let prover = Prover::new(rcot_recv);
|
||||
let verifier = Verifier::new(delta_zk, rcot_send);
|
||||
let prover = Prover::new(ProverConfig::default(), rcot_recv);
|
||||
let verifier = Verifier::new(VerifierConfig::default(), delta_zk, rcot_send);
|
||||
|
||||
let mut leader = Deap::new(Role::Leader, gb, prover);
|
||||
let mut follower = Deap::new(Role::Follower, ev, verifier);
|
||||
@@ -488,8 +488,8 @@ mod tests {
|
||||
|
||||
let gb = Garbler::new(cot_send, [0u8; 16], delta_mpc);
|
||||
let ev = Evaluator::new(cot_recv);
|
||||
let prover = Prover::new(rcot_recv);
|
||||
let verifier = Verifier::new(delta_zk, rcot_send);
|
||||
let prover = Prover::new(ProverConfig::default(), rcot_recv);
|
||||
let verifier = Verifier::new(VerifierConfig::default(), delta_zk, rcot_send);
|
||||
|
||||
let mut leader = Deap::new(Role::Leader, gb, prover);
|
||||
let mut follower = Deap::new(Role::Follower, ev, verifier);
|
||||
@@ -574,8 +574,8 @@ mod tests {
|
||||
|
||||
let gb = Garbler::new(cot_send, [1u8; 16], delta_mpc);
|
||||
let ev = Evaluator::new(cot_recv);
|
||||
let prover = Prover::new(rcot_recv);
|
||||
let verifier = Verifier::new(delta_zk, rcot_send);
|
||||
let prover = Prover::new(ProverConfig::default(), rcot_recv);
|
||||
let verifier = Verifier::new(VerifierConfig::default(), delta_zk, rcot_send);
|
||||
|
||||
let mut leader = Deap::new(Role::Leader, gb, prover);
|
||||
let mut follower = Deap::new(Role::Follower, ev, verifier);
|
||||
|
||||
@@ -40,7 +40,6 @@ enum PrfState {
|
||||
inner_partial: [u32; 8],
|
||||
a_output: DecodeFutureTyped<BitVec, [u8; 32]>,
|
||||
},
|
||||
FinishLastP,
|
||||
Done,
|
||||
}
|
||||
|
||||
@@ -137,16 +136,18 @@ impl PrfFunction {
|
||||
assign_inner_local(vm, p.inner_local, *inner_partial, &msg)?;
|
||||
|
||||
if *iter == self.iterations {
|
||||
self.state = PrfState::FinishLastP;
|
||||
self.state = PrfState::Done;
|
||||
} else {
|
||||
self.state = PrfState::ComputeA {
|
||||
iter: *iter + 1,
|
||||
inner_partial: *inner_partial,
|
||||
msg: output.to_vec(),
|
||||
}
|
||||
};
|
||||
};
|
||||
// We recurse, so that this PHash and the next AHash could
|
||||
// be computed in a single VM execute call.
|
||||
self.flush(vm)?;
|
||||
}
|
||||
}
|
||||
PrfState::FinishLastP => self.state = PrfState::Done,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,13 @@ workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
fixtures = ["dep:hex", "dep:tlsn-data-fixtures"]
|
||||
fixtures = [
|
||||
"dep:hex",
|
||||
"dep:tlsn-data-fixtures",
|
||||
"dep:aead",
|
||||
"dep:aes-gcm",
|
||||
"dep:generic-array",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
tlsn-data-fixtures = { workspace = true, optional = true }
|
||||
@@ -21,6 +27,9 @@ tlsn-tls-core = { workspace = true, features = ["serde"] }
|
||||
tlsn-utils = { workspace = true }
|
||||
rangeset = { workspace = true, features = ["serde"] }
|
||||
|
||||
aead = { workspace = true, features = ["alloc"], optional = true }
|
||||
aes-gcm = { workspace = true, optional = true }
|
||||
generic-array = { workspace = true, optional = true }
|
||||
bimap = { version = "0.6", features = ["serde"] }
|
||||
blake3 = { workspace = true }
|
||||
hex = { workspace = true, optional = true }
|
||||
@@ -36,10 +45,17 @@ thiserror = { workspace = true }
|
||||
tiny-keccak = { workspace = true, features = ["keccak"] }
|
||||
web-time = { workspace = true }
|
||||
webpki-roots = { workspace = true }
|
||||
rustls-webpki = { workspace = true, features = ["ring"] }
|
||||
rustls-pki-types = { workspace = true }
|
||||
itybity = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
aead = { workspace = true, features = ["alloc"] }
|
||||
aes-gcm = { workspace = true }
|
||||
generic-array = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
tlsn-data-fixtures = { workspace = true }
|
||||
webpki-root-certs = { workspace = true }
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use rustls_pki_types as webpki_types;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tls_core::{
|
||||
msgs::{
|
||||
codec::Codec,
|
||||
enums::NamedGroup,
|
||||
handshake::{DigitallySignedStruct, ServerECDHParams},
|
||||
},
|
||||
verify::{ServerCertVerifier as _, WebPkiVerifier},
|
||||
};
|
||||
use web_time::{Duration, UNIX_EPOCH};
|
||||
use tls_core::msgs::{codec::Codec, enums::NamedGroup, handshake::ServerECDHParams};
|
||||
|
||||
use crate::webpki::{CertificateDer, ServerCertVerifier, ServerCertVerifierError};
|
||||
|
||||
/// TLS version.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
@@ -35,40 +30,82 @@ impl TryFrom<tls_core::msgs::enums::ProtocolVersion> for TlsVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/// Server's name, a.k.a. the DNS name.
|
||||
/// Server's name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct ServerName(String);
|
||||
pub enum ServerName {
|
||||
/// DNS name.
|
||||
Dns(DnsName),
|
||||
}
|
||||
|
||||
impl ServerName {
|
||||
/// Creates a new server name.
|
||||
pub fn new(name: String) -> Self {
|
||||
Self(name)
|
||||
}
|
||||
|
||||
/// Returns the name as a string.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ServerName {
|
||||
fn from(name: &str) -> Self {
|
||||
Self(name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ServerName {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
pub(crate) fn to_webpki(&self) -> webpki_types::ServerName<'static> {
|
||||
match self {
|
||||
ServerName::Dns(name) => webpki_types::ServerName::DnsName(
|
||||
webpki_types::DnsName::try_from(name.0.as_str())
|
||||
.expect("name was validated")
|
||||
.to_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ServerName::Dns(name) => write!(f, "{name}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DNS name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String")]
|
||||
pub struct DnsName(String);
|
||||
|
||||
impl DnsName {
|
||||
/// Returns the DNS name as a string.
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DnsName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for DnsName {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when a DNS name is invalid.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("invalid DNS name")]
|
||||
pub struct InvalidDnsNameError {}
|
||||
|
||||
impl TryFrom<&str> for DnsName {
|
||||
type Error = InvalidDnsNameError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
// Borrow validation from rustls
|
||||
match webpki_types::DnsName::try_from_str(value) {
|
||||
Ok(_) => Ok(DnsName(value.to_string())),
|
||||
Err(_) => Err(InvalidDnsNameError {}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for DnsName {
|
||||
type Error = InvalidDnsNameError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of a public key.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -98,6 +135,25 @@ pub enum SignatureScheme {
|
||||
ED25519 = 0x0807,
|
||||
}
|
||||
|
||||
impl fmt::Display for SignatureScheme {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SignatureScheme::RSA_PKCS1_SHA1 => write!(f, "RSA_PKCS1_SHA1"),
|
||||
SignatureScheme::ECDSA_SHA1_Legacy => write!(f, "ECDSA_SHA1_Legacy"),
|
||||
SignatureScheme::RSA_PKCS1_SHA256 => write!(f, "RSA_PKCS1_SHA256"),
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256 => write!(f, "ECDSA_NISTP256_SHA256"),
|
||||
SignatureScheme::RSA_PKCS1_SHA384 => write!(f, "RSA_PKCS1_SHA384"),
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384 => write!(f, "ECDSA_NISTP384_SHA384"),
|
||||
SignatureScheme::RSA_PKCS1_SHA512 => write!(f, "RSA_PKCS1_SHA512"),
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512 => write!(f, "ECDSA_NISTP521_SHA512"),
|
||||
SignatureScheme::RSA_PSS_SHA256 => write!(f, "RSA_PSS_SHA256"),
|
||||
SignatureScheme::RSA_PSS_SHA384 => write!(f, "RSA_PSS_SHA384"),
|
||||
SignatureScheme::RSA_PSS_SHA512 => write!(f, "RSA_PSS_SHA512"),
|
||||
SignatureScheme::ED25519 => write!(f, "ED25519"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<tls_core::msgs::enums::SignatureScheme> for SignatureScheme {
|
||||
type Error = &'static str;
|
||||
|
||||
@@ -142,16 +198,6 @@ impl From<SignatureScheme> for tls_core::msgs::enums::SignatureScheme {
|
||||
}
|
||||
}
|
||||
|
||||
/// X.509 certificate, DER encoded.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Certificate(pub Vec<u8>);
|
||||
|
||||
impl From<tls_core::key::Certificate> for Certificate {
|
||||
fn from(cert: tls_core::key::Certificate) -> Self {
|
||||
Self(cert.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Server's signature of the key exchange parameters.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerSignature {
|
||||
@@ -220,9 +266,9 @@ pub struct TranscriptLength {
|
||||
pub received: u32,
|
||||
}
|
||||
|
||||
/// TLS 1.2 handshake data.
|
||||
/// TLS 1.2 certificate binding.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HandshakeDataV1_2 {
|
||||
pub struct CertBindingV1_2 {
|
||||
/// Client random.
|
||||
pub client_random: [u8; 32],
|
||||
/// Server random.
|
||||
@@ -231,13 +277,18 @@ pub struct HandshakeDataV1_2 {
|
||||
pub server_ephemeral_key: ServerEphemKey,
|
||||
}
|
||||
|
||||
/// TLS handshake data.
|
||||
/// TLS certificate binding.
|
||||
///
|
||||
/// This is the data that the server signs using its public key in the
|
||||
/// certificate it presents during the TLS handshake. This provides a binding
|
||||
/// between the server's identity and the ephemeral keys used to authenticate
|
||||
/// the TLS session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[non_exhaustive]
|
||||
pub enum HandshakeData {
|
||||
/// TLS 1.2 handshake data.
|
||||
V1_2(HandshakeDataV1_2),
|
||||
pub enum CertBinding {
|
||||
/// TLS 1.2 certificate binding.
|
||||
V1_2(CertBindingV1_2),
|
||||
}
|
||||
|
||||
/// Verify data from the TLS handshake finished messages.
|
||||
@@ -249,19 +300,19 @@ pub struct VerifyData {
|
||||
pub server_finished: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Server certificate and handshake data.
|
||||
/// TLS handshake data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerCertData {
|
||||
/// Certificate chain.
|
||||
pub certs: Vec<Certificate>,
|
||||
/// Server signature of the key exchange parameters.
|
||||
pub struct HandshakeData {
|
||||
/// Server certificate chain.
|
||||
pub certs: Vec<CertificateDer>,
|
||||
/// Server certificate signature over the binding message.
|
||||
pub sig: ServerSignature,
|
||||
/// TLS handshake data.
|
||||
pub handshake: HandshakeData,
|
||||
/// Certificate binding.
|
||||
pub binding: CertBinding,
|
||||
}
|
||||
|
||||
impl ServerCertData {
|
||||
/// Verifies the server certificate data.
|
||||
impl HandshakeData {
|
||||
/// Verifies the handshake data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -271,53 +322,35 @@ impl ServerCertData {
|
||||
/// * `server_name` - The server name.
|
||||
pub fn verify(
|
||||
&self,
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
time: u64,
|
||||
server_ephemeral_key: &ServerEphemKey,
|
||||
server_name: &ServerName,
|
||||
) -> Result<(), CertificateVerificationError> {
|
||||
) -> Result<(), HandshakeVerificationError> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
let HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let CertBinding::V1_2(CertBindingV1_2 {
|
||||
client_random,
|
||||
server_random,
|
||||
server_ephemeral_key: expected_server_ephemeral_key,
|
||||
}) = &self.handshake
|
||||
}) = &self.binding
|
||||
else {
|
||||
unreachable!("only TLS 1.2 is implemented")
|
||||
};
|
||||
|
||||
if server_ephemeral_key != expected_server_ephemeral_key {
|
||||
return Err(CertificateVerificationError::InvalidServerEphemeralKey);
|
||||
return Err(HandshakeVerificationError::InvalidServerEphemeralKey);
|
||||
}
|
||||
|
||||
// Verify server name.
|
||||
let server_name = tls_core::dns::ServerName::try_from(server_name.as_ref())
|
||||
.map_err(|_| CertificateVerificationError::InvalidIdentity(server_name.clone()))?;
|
||||
|
||||
// Verify server certificate.
|
||||
let cert_chain = self
|
||||
let (end_entity, intermediates) = self
|
||||
.certs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|cert| tls_core::key::Certificate(cert.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (end_entity, intermediates) = cert_chain
|
||||
.split_first()
|
||||
.ok_or(CertificateVerificationError::MissingCerts)?;
|
||||
.ok_or(HandshakeVerificationError::MissingCerts)?;
|
||||
|
||||
// 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.
|
||||
verifier
|
||||
.verify_server_cert(
|
||||
end_entity,
|
||||
intermediates,
|
||||
&server_name,
|
||||
&mut [].into_iter(),
|
||||
&[],
|
||||
UNIX_EPOCH + Duration::from_secs(time),
|
||||
)
|
||||
.map_err(|_| CertificateVerificationError::InvalidCert)?;
|
||||
.verify_server_cert(end_entity, intermediates, server_name, time)
|
||||
.map_err(HandshakeVerificationError::ServerCert)?;
|
||||
|
||||
// Verify the signature matches the certificate and key exchange parameters.
|
||||
let mut message = Vec::new();
|
||||
@@ -325,11 +358,31 @@ impl ServerCertData {
|
||||
message.extend_from_slice(server_random);
|
||||
message.extend_from_slice(&server_ephemeral_key.kx_params());
|
||||
|
||||
let dss = DigitallySignedStruct::new(self.sig.scheme.into(), self.sig.sig.clone());
|
||||
use webpki::ring as alg;
|
||||
let sig_alg = match self.sig.scheme {
|
||||
SignatureScheme::RSA_PKCS1_SHA256 => alg::RSA_PKCS1_2048_8192_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384 => alg::RSA_PKCS1_2048_8192_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512 => alg::RSA_PKCS1_2048_8192_SHA512,
|
||||
SignatureScheme::RSA_PSS_SHA256 => alg::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
|
||||
SignatureScheme::RSA_PSS_SHA384 => alg::RSA_PSS_2048_8192_SHA384_LEGACY_KEY,
|
||||
SignatureScheme::RSA_PSS_SHA512 => alg::RSA_PSS_2048_8192_SHA512_LEGACY_KEY,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256 => alg::ECDSA_P256_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384 => alg::ECDSA_P384_SHA384,
|
||||
SignatureScheme::ED25519 => alg::ED25519,
|
||||
scheme => {
|
||||
return Err(HandshakeVerificationError::UnsupportedSignatureScheme(
|
||||
scheme,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
verifier
|
||||
.verify_tls12_signature(&message, end_entity, &dss)
|
||||
.map_err(|_| CertificateVerificationError::InvalidServerSignature)?;
|
||||
let end_entity = webpki_types::CertificateDer::from(end_entity.0.as_slice());
|
||||
let end_entity = webpki::EndEntityCert::try_from(&end_entity)
|
||||
.map_err(|_| HandshakeVerificationError::InvalidEndEntityCertificate)?;
|
||||
|
||||
end_entity
|
||||
.verify_signature(sig_alg, &message, &self.sig.sig)
|
||||
.map_err(|_| HandshakeVerificationError::InvalidServerSignature)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -338,58 +391,51 @@ impl ServerCertData {
|
||||
/// Errors that can occur when verifying a certificate chain or signature.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum CertificateVerificationError {
|
||||
#[error("invalid server identity: {0}")]
|
||||
InvalidIdentity(ServerName),
|
||||
pub enum HandshakeVerificationError {
|
||||
#[error("invalid end entity certificate")]
|
||||
InvalidEndEntityCertificate,
|
||||
#[error("missing server certificates")]
|
||||
MissingCerts,
|
||||
#[error("invalid server certificate")]
|
||||
InvalidCert,
|
||||
#[error("invalid server signature")]
|
||||
InvalidServerSignature,
|
||||
#[error("invalid server ephemeral key")]
|
||||
InvalidServerEphemeralKey,
|
||||
#[error("server certificate verification failed: {0}")]
|
||||
ServerCert(ServerCertVerifierError),
|
||||
#[error("unsupported signature scheme: {0}")]
|
||||
UnsupportedSignatureScheme(SignatureScheme),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{fixtures::ConnectionFixture, transcript::Transcript};
|
||||
use crate::{fixtures::ConnectionFixture, transcript::Transcript, webpki::RootCertStore};
|
||||
|
||||
use hex::FromHex;
|
||||
use rstest::*;
|
||||
use tls_core::{
|
||||
anchors::{OwnedTrustAnchor, RootCertStore},
|
||||
verify::WebPkiVerifier,
|
||||
};
|
||||
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
|
||||
|
||||
#[fixture]
|
||||
#[once]
|
||||
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()),
|
||||
)
|
||||
}));
|
||||
fn verifier() -> ServerCertVerifier {
|
||||
let mut root_store = RootCertStore {
|
||||
roots: webpki_root_certs::TLS_SERVER_ROOT_CERTS
|
||||
.iter()
|
||||
.map(|c| CertificateDer(c.to_vec()))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// Add a cert which is no longer included in the Mozilla root store.
|
||||
let cert = tls_core::key::Certificate(
|
||||
root_store.roots.push(
|
||||
appliedzkp()
|
||||
.server_cert_data
|
||||
.certs
|
||||
.last()
|
||||
.expect("chain is valid")
|
||||
.0
|
||||
.clone(),
|
||||
);
|
||||
|
||||
root_store.add(&cert).unwrap();
|
||||
|
||||
WebPkiVerifier::new(root_store, None)
|
||||
ServerCertVerifier::new(&root_store).unwrap()
|
||||
}
|
||||
|
||||
fn tlsnotary() -> ConnectionFixture {
|
||||
@@ -405,7 +451,7 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_cert_chain_sucess_ca_implicit(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] mut data: ConnectionFixture,
|
||||
) {
|
||||
// Remove the CA cert
|
||||
@@ -417,7 +463,7 @@ mod tests {
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
@@ -428,7 +474,7 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_cert_chain_success_ca_explicit(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] data: ConnectionFixture,
|
||||
) {
|
||||
assert!(data
|
||||
@@ -437,7 +483,7 @@ mod tests {
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
@@ -447,7 +493,7 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_cert_chain_fail_bad_time(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] data: ConnectionFixture,
|
||||
) {
|
||||
// unix time when the cert chain was NOT valid
|
||||
@@ -457,12 +503,12 @@ mod tests {
|
||||
verifier,
|
||||
bad_time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidCert
|
||||
HandshakeVerificationError::ServerCert(_)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -471,7 +517,7 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_cert_chain_fail_no_interm_cert(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] mut data: ConnectionFixture,
|
||||
) {
|
||||
// Remove the CA cert
|
||||
@@ -483,12 +529,12 @@ mod tests {
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidCert
|
||||
HandshakeVerificationError::ServerCert(_)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -498,7 +544,7 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_cert_chain_fail_no_interm_cert_with_ca_cert(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] mut data: ConnectionFixture,
|
||||
) {
|
||||
// Remove the intermediate cert
|
||||
@@ -508,12 +554,12 @@ mod tests {
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidCert
|
||||
HandshakeVerificationError::ServerCert(_)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -522,24 +568,24 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_cert_chain_fail_bad_ee_cert(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] mut data: ConnectionFixture,
|
||||
) {
|
||||
let ee: &[u8] = include_bytes!("./fixtures/data/unknown/ee.der");
|
||||
|
||||
// Change the end entity cert
|
||||
data.server_cert_data.certs[0] = Certificate(ee.to_vec());
|
||||
data.server_cert_data.certs[0] = CertificateDer(ee.to_vec());
|
||||
|
||||
let err = data.server_cert_data.verify(
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidCert
|
||||
HandshakeVerificationError::ServerCert(_)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -548,23 +594,23 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_sig_ke_params_fail_bad_client_random(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] mut data: ConnectionFixture,
|
||||
) {
|
||||
let HandshakeData::V1_2(HandshakeDataV1_2 { client_random, .. }) =
|
||||
&mut data.server_cert_data.handshake;
|
||||
let CertBinding::V1_2(CertBindingV1_2 { client_random, .. }) =
|
||||
&mut data.server_cert_data.binding;
|
||||
client_random[31] = client_random[31].wrapping_add(1);
|
||||
|
||||
let err = data.server_cert_data.verify(
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidServerSignature
|
||||
HandshakeVerificationError::InvalidServerSignature
|
||||
));
|
||||
}
|
||||
|
||||
@@ -573,7 +619,7 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_sig_ke_params_fail_bad_sig(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] mut data: ConnectionFixture,
|
||||
) {
|
||||
data.server_cert_data.sig.sig[31] = data.server_cert_data.sig.sig[31].wrapping_add(1);
|
||||
@@ -582,12 +628,12 @@ mod tests {
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidServerSignature
|
||||
HandshakeVerificationError::InvalidServerSignature
|
||||
));
|
||||
}
|
||||
|
||||
@@ -596,10 +642,10 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_check_dns_name_present_in_cert_fail_bad_host(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] data: ConnectionFixture,
|
||||
) {
|
||||
let bad_name = ServerName::from("badhost.com");
|
||||
let bad_name = ServerName::Dns(DnsName::try_from("badhost.com").unwrap());
|
||||
|
||||
let err = data.server_cert_data.verify(
|
||||
verifier,
|
||||
@@ -610,7 +656,7 @@ mod tests {
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidCert
|
||||
HandshakeVerificationError::ServerCert(_)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -618,7 +664,7 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_invalid_ephemeral_key(verifier: &WebPkiVerifier, #[case] data: ConnectionFixture) {
|
||||
fn test_invalid_ephemeral_key(verifier: &ServerCertVerifier, #[case] data: ConnectionFixture) {
|
||||
let wrong_ephemeral_key = ServerEphemKey {
|
||||
typ: KeyType::SECP256R1,
|
||||
key: Vec::<u8>::from_hex(include_bytes!("./fixtures/data/unknown/pubkey")).unwrap(),
|
||||
@@ -628,12 +674,12 @@ mod tests {
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
&wrong_ephemeral_key,
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::InvalidServerEphemeralKey
|
||||
HandshakeVerificationError::InvalidServerEphemeralKey
|
||||
));
|
||||
}
|
||||
|
||||
@@ -642,7 +688,7 @@ mod tests {
|
||||
#[case::tlsnotary(tlsnotary())]
|
||||
#[case::appliedzkp(appliedzkp())]
|
||||
fn test_verify_cert_chain_fail_no_cert(
|
||||
verifier: &WebPkiVerifier,
|
||||
verifier: &ServerCertVerifier,
|
||||
#[case] mut data: ConnectionFixture,
|
||||
) {
|
||||
// Empty certs
|
||||
@@ -652,12 +698,12 @@ mod tests {
|
||||
verifier,
|
||||
data.connection_info.time,
|
||||
data.server_ephemeral_key(),
|
||||
&ServerName::from(data.server_name.as_ref()),
|
||||
&data.server_name,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err.unwrap_err(),
|
||||
CertificateVerificationError::MissingCerts
|
||||
HandshakeVerificationError::MissingCerts
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
16
crates/core/src/display.rs
Normal file
16
crates/core/src/display.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use rangeset::RangeSet;
|
||||
|
||||
pub(crate) struct FmtRangeSet<'a>(pub &'a RangeSet<usize>);
|
||||
|
||||
impl<'a> std::fmt::Display for FmtRangeSet<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("{")?;
|
||||
for range in self.0.iter_ranges() {
|
||||
write!(f, "{}..{}", range.start, range.end)?;
|
||||
if range.end < self.0.end().unwrap_or(0) {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
}
|
||||
f.write_str("}")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Fixtures for testing
|
||||
|
||||
mod provider;
|
||||
pub mod transcript;
|
||||
|
||||
pub use provider::FixtureEncodingProvider;
|
||||
|
||||
@@ -8,13 +9,14 @@ use hex::FromHex;
|
||||
|
||||
use crate::{
|
||||
connection::{
|
||||
Certificate, ConnectionInfo, HandshakeData, HandshakeDataV1_2, KeyType, ServerCertData,
|
||||
CertBinding, CertBindingV1_2, ConnectionInfo, DnsName, HandshakeData, KeyType,
|
||||
ServerEphemKey, ServerName, ServerSignature, SignatureScheme, TlsVersion, TranscriptLength,
|
||||
},
|
||||
transcript::{
|
||||
encoding::{EncoderSecret, EncodingProvider},
|
||||
Transcript,
|
||||
},
|
||||
webpki::CertificateDer,
|
||||
};
|
||||
|
||||
/// A fixture containing various TLS connection data.
|
||||
@@ -23,24 +25,26 @@ use crate::{
|
||||
pub struct ConnectionFixture {
|
||||
pub server_name: ServerName,
|
||||
pub connection_info: ConnectionInfo,
|
||||
pub server_cert_data: ServerCertData,
|
||||
pub server_cert_data: HandshakeData,
|
||||
}
|
||||
|
||||
impl ConnectionFixture {
|
||||
/// Returns a connection fixture for tlsnotary.org.
|
||||
pub fn tlsnotary(transcript_length: TranscriptLength) -> Self {
|
||||
ConnectionFixture {
|
||||
server_name: ServerName::new("tlsnotary.org".to_string()),
|
||||
server_name: ServerName::Dns(DnsName::try_from("tlsnotary.org").unwrap()),
|
||||
connection_info: ConnectionInfo {
|
||||
time: 1671637529,
|
||||
version: TlsVersion::V1_2,
|
||||
transcript_length,
|
||||
},
|
||||
server_cert_data: ServerCertData {
|
||||
server_cert_data: HandshakeData {
|
||||
certs: vec![
|
||||
Certificate(include_bytes!("fixtures/data/tlsnotary.org/ee.der").to_vec()),
|
||||
Certificate(include_bytes!("fixtures/data/tlsnotary.org/inter.der").to_vec()),
|
||||
Certificate(include_bytes!("fixtures/data/tlsnotary.org/ca.der").to_vec()),
|
||||
CertificateDer(include_bytes!("fixtures/data/tlsnotary.org/ee.der").to_vec()),
|
||||
CertificateDer(
|
||||
include_bytes!("fixtures/data/tlsnotary.org/inter.der").to_vec(),
|
||||
),
|
||||
CertificateDer(include_bytes!("fixtures/data/tlsnotary.org/ca.der").to_vec()),
|
||||
],
|
||||
sig: ServerSignature {
|
||||
scheme: SignatureScheme::RSA_PKCS1_SHA256,
|
||||
@@ -49,7 +53,7 @@ impl ConnectionFixture {
|
||||
))
|
||||
.unwrap(),
|
||||
},
|
||||
handshake: HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
binding: CertBinding::V1_2(CertBindingV1_2 {
|
||||
client_random: <[u8; 32]>::from_hex(include_bytes!(
|
||||
"fixtures/data/tlsnotary.org/client_random"
|
||||
))
|
||||
@@ -73,17 +77,19 @@ impl ConnectionFixture {
|
||||
/// Returns a connection fixture for appliedzkp.org.
|
||||
pub fn appliedzkp(transcript_length: TranscriptLength) -> Self {
|
||||
ConnectionFixture {
|
||||
server_name: ServerName::new("appliedzkp.org".to_string()),
|
||||
server_name: ServerName::Dns(DnsName::try_from("appliedzkp.org").unwrap()),
|
||||
connection_info: ConnectionInfo {
|
||||
time: 1671637529,
|
||||
version: TlsVersion::V1_2,
|
||||
transcript_length,
|
||||
},
|
||||
server_cert_data: ServerCertData {
|
||||
server_cert_data: HandshakeData {
|
||||
certs: vec![
|
||||
Certificate(include_bytes!("fixtures/data/appliedzkp.org/ee.der").to_vec()),
|
||||
Certificate(include_bytes!("fixtures/data/appliedzkp.org/inter.der").to_vec()),
|
||||
Certificate(include_bytes!("fixtures/data/appliedzkp.org/ca.der").to_vec()),
|
||||
CertificateDer(include_bytes!("fixtures/data/appliedzkp.org/ee.der").to_vec()),
|
||||
CertificateDer(
|
||||
include_bytes!("fixtures/data/appliedzkp.org/inter.der").to_vec(),
|
||||
),
|
||||
CertificateDer(include_bytes!("fixtures/data/appliedzkp.org/ca.der").to_vec()),
|
||||
],
|
||||
sig: ServerSignature {
|
||||
scheme: SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
@@ -92,7 +98,7 @@ impl ConnectionFixture {
|
||||
))
|
||||
.unwrap(),
|
||||
},
|
||||
handshake: HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
binding: CertBinding::V1_2(CertBindingV1_2 {
|
||||
client_random: <[u8; 32]>::from_hex(include_bytes!(
|
||||
"fixtures/data/appliedzkp.org/client_random"
|
||||
))
|
||||
@@ -115,10 +121,10 @@ impl ConnectionFixture {
|
||||
|
||||
/// Returns the server_ephemeral_key fixture.
|
||||
pub fn server_ephemeral_key(&self) -> &ServerEphemKey {
|
||||
let HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let CertBinding::V1_2(CertBindingV1_2 {
|
||||
server_ephemeral_key,
|
||||
..
|
||||
}) = &self.server_cert_data.handshake;
|
||||
}) = &self.server_cert_data.binding;
|
||||
server_ephemeral_key
|
||||
}
|
||||
}
|
||||
|
||||
199
crates/core/src/fixtures/transcript.rs
Normal file
199
crates/core/src/fixtures/transcript.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
//! Transcript fixtures for testing.
|
||||
|
||||
use aead::Payload as AeadPayload;
|
||||
use aes_gcm::{aead::Aead, Aes128Gcm, NewAead};
|
||||
use generic_array::GenericArray;
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
use tls_core::msgs::{
|
||||
base::Payload,
|
||||
codec::Codec,
|
||||
enums::{ContentType, HandshakeType, ProtocolVersion},
|
||||
handshake::{HandshakeMessagePayload, HandshakePayload},
|
||||
message::{OpaqueMessage, PlainMessage},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
connection::{TranscriptLength, VerifyData},
|
||||
fixtures::ConnectionFixture,
|
||||
transcript::{Record, TlsTranscript},
|
||||
};
|
||||
|
||||
/// The key used for encryption of the sent and received transcript.
|
||||
pub const KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
|
||||
/// The iv used for encryption of the sent and received transcript.
|
||||
pub const IV: [u8; 4] = [1, 3, 3, 7];
|
||||
|
||||
/// The record size in bytes.
|
||||
pub const RECORD_SIZE: usize = 512;
|
||||
|
||||
/// Creates a transript fixture for testing.
|
||||
pub fn transcript_fixture(sent: &[u8], recv: &[u8]) -> TlsTranscript {
|
||||
TranscriptGenerator::new(KEY, IV).generate(sent, recv)
|
||||
}
|
||||
|
||||
struct TranscriptGenerator {
|
||||
key: [u8; 16],
|
||||
iv: [u8; 4],
|
||||
}
|
||||
|
||||
impl TranscriptGenerator {
|
||||
fn new(key: [u8; 16], iv: [u8; 4]) -> Self {
|
||||
Self { key, iv }
|
||||
}
|
||||
|
||||
fn generate(&self, sent: &[u8], recv: &[u8]) -> TlsTranscript {
|
||||
let mut rng = StdRng::from_seed([1; 32]);
|
||||
|
||||
let transcript_len = TranscriptLength {
|
||||
sent: sent.len() as u32,
|
||||
received: recv.len() as u32,
|
||||
};
|
||||
let tlsn = ConnectionFixture::tlsnotary(transcript_len);
|
||||
|
||||
let time = tlsn.connection_info.time;
|
||||
let version = tlsn.connection_info.version;
|
||||
let server_cert_chain = tlsn.server_cert_data.certs;
|
||||
let server_signature = tlsn.server_cert_data.sig;
|
||||
let cert_binding = tlsn.server_cert_data.binding;
|
||||
|
||||
let cf_vd: [u8; 12] = rng.random();
|
||||
let sf_vd: [u8; 12] = rng.random();
|
||||
|
||||
let verify_data = VerifyData {
|
||||
client_finished: cf_vd.to_vec(),
|
||||
server_finished: sf_vd.to_vec(),
|
||||
};
|
||||
|
||||
let sent = self.gen_records(cf_vd, sent);
|
||||
let recv = self.gen_records(sf_vd, recv);
|
||||
|
||||
TlsTranscript::new(
|
||||
time,
|
||||
version,
|
||||
Some(server_cert_chain),
|
||||
Some(server_signature),
|
||||
cert_binding,
|
||||
verify_data,
|
||||
sent,
|
||||
recv,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn gen_records(&self, vd: [u8; 12], plaintext: &[u8]) -> Vec<Record> {
|
||||
let mut records = Vec::new();
|
||||
|
||||
let handshake = self.gen_handshake(vd);
|
||||
records.push(handshake);
|
||||
|
||||
for (seq, msg) in (1_u64..).zip(plaintext.chunks(RECORD_SIZE)) {
|
||||
let record = self.gen_app_data(seq, msg);
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
records
|
||||
}
|
||||
|
||||
fn gen_app_data(&self, seq: u64, plaintext: &[u8]) -> Record {
|
||||
assert!(
|
||||
plaintext.len() <= 1 << 14,
|
||||
"plaintext len per record must be smaller than 2^14 bytes"
|
||||
);
|
||||
|
||||
let explicit_nonce: [u8; 8] = seq.to_be_bytes();
|
||||
let msg = PlainMessage {
|
||||
typ: ContentType::ApplicationData,
|
||||
version: ProtocolVersion::TLSv1_2,
|
||||
payload: Payload::new(plaintext),
|
||||
};
|
||||
let opaque = aes_gcm_encrypt(self.key, self.iv, seq, explicit_nonce, &msg);
|
||||
|
||||
let mut payload = opaque.payload.0;
|
||||
let mut ciphertext = payload.split_off(8);
|
||||
let tag = ciphertext.split_off(ciphertext.len() - 16);
|
||||
|
||||
Record {
|
||||
seq,
|
||||
typ: ContentType::ApplicationData,
|
||||
plaintext: Some(plaintext.to_vec()),
|
||||
explicit_nonce: explicit_nonce.to_vec(),
|
||||
ciphertext,
|
||||
tag: Some(tag),
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_handshake(&self, vd: [u8; 12]) -> Record {
|
||||
let seq = 0_u64;
|
||||
let explicit_nonce = seq.to_be_bytes();
|
||||
|
||||
let mut plaintext = Vec::new();
|
||||
|
||||
let payload = Payload(vd.to_vec());
|
||||
let hs_payload = HandshakePayload::Finished(payload);
|
||||
let handshake_message = HandshakeMessagePayload {
|
||||
typ: HandshakeType::Finished,
|
||||
payload: hs_payload,
|
||||
};
|
||||
handshake_message.encode(&mut plaintext);
|
||||
|
||||
let msg = PlainMessage {
|
||||
typ: ContentType::Handshake,
|
||||
version: ProtocolVersion::TLSv1_2,
|
||||
payload: Payload::new(plaintext.clone()),
|
||||
};
|
||||
|
||||
let opaque = aes_gcm_encrypt(self.key, self.iv, seq, explicit_nonce, &msg);
|
||||
let mut payload = opaque.payload.0;
|
||||
let mut ciphertext = payload.split_off(8);
|
||||
let tag = ciphertext.split_off(ciphertext.len() - 16);
|
||||
|
||||
Record {
|
||||
seq,
|
||||
typ: ContentType::Handshake,
|
||||
plaintext: Some(plaintext),
|
||||
explicit_nonce: explicit_nonce.to_vec(),
|
||||
ciphertext,
|
||||
tag: Some(tag),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn aes_gcm_encrypt(
|
||||
key: [u8; 16],
|
||||
iv: [u8; 4],
|
||||
seq: u64,
|
||||
explicit_nonce: [u8; 8],
|
||||
msg: &PlainMessage,
|
||||
) -> OpaqueMessage {
|
||||
let mut aad = [0u8; 13];
|
||||
|
||||
aad[..8].copy_from_slice(&seq.to_be_bytes());
|
||||
aad[8] = msg.typ.get_u8();
|
||||
aad[9..11].copy_from_slice(&msg.version.get_u16().to_be_bytes());
|
||||
aad[11..13].copy_from_slice(&(msg.payload.0.len() as u16).to_be_bytes());
|
||||
let payload = AeadPayload {
|
||||
msg: &msg.payload.0,
|
||||
aad: &aad,
|
||||
};
|
||||
|
||||
let mut nonce = [0u8; 12];
|
||||
nonce[..4].copy_from_slice(&iv);
|
||||
nonce[4..].copy_from_slice(&explicit_nonce);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
let cipher = Aes128Gcm::new_from_slice(&key).unwrap();
|
||||
|
||||
// ciphertext will have the MAC appended
|
||||
let ciphertext = cipher.encrypt(nonce, payload).unwrap();
|
||||
|
||||
// prepend the explicit nonce
|
||||
let mut nonce_ct_mac = vec![0u8; 0];
|
||||
nonce_ct_mac.extend(explicit_nonce.iter());
|
||||
nonce_ct_mac.extend(ciphertext.iter());
|
||||
|
||||
OpaqueMessage {
|
||||
typ: msg.typ,
|
||||
version: msg.version,
|
||||
payload: Payload::new(nonce_ct_mac),
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,11 @@ impl Hash {
|
||||
len: value.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a byte slice of the hash value.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.value[..self.len]
|
||||
}
|
||||
}
|
||||
|
||||
impl rs_merkle::Hash for Hash {
|
||||
|
||||
@@ -10,29 +10,32 @@ pub mod fixtures;
|
||||
pub mod hash;
|
||||
pub mod merkle;
|
||||
pub mod transcript;
|
||||
pub mod webpki;
|
||||
pub use rangeset;
|
||||
pub(crate) mod display;
|
||||
|
||||
use rangeset::ToRangeSet;
|
||||
use rangeset::{RangeSet, ToRangeSet, UnionMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connection::{ServerCertData, ServerName},
|
||||
connection::{HandshakeData, ServerName},
|
||||
transcript::{
|
||||
Direction, Idx, PartialTranscript, Transcript, TranscriptCommitConfig,
|
||||
TranscriptCommitRequest, TranscriptCommitment, TranscriptSecret,
|
||||
Direction, PartialTranscript, Transcript, TranscriptCommitConfig, TranscriptCommitRequest,
|
||||
TranscriptCommitment, TranscriptSecret,
|
||||
},
|
||||
};
|
||||
|
||||
/// Configuration to prove information to the verifier.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProveConfig {
|
||||
server_identity: bool,
|
||||
transcript: Option<PartialTranscript>,
|
||||
reveal: Option<(RangeSet<usize>, RangeSet<usize>)>,
|
||||
transcript_commit: Option<TranscriptCommitConfig>,
|
||||
}
|
||||
|
||||
impl ProveConfig {
|
||||
/// Creates a new builder.
|
||||
pub fn builder(transcript: &Transcript) -> ProveConfigBuilder {
|
||||
pub fn builder(transcript: &Transcript) -> ProveConfigBuilder<'_> {
|
||||
ProveConfigBuilder::new(transcript)
|
||||
}
|
||||
|
||||
@@ -41,9 +44,9 @@ impl ProveConfig {
|
||||
self.server_identity
|
||||
}
|
||||
|
||||
/// Returns the transcript to be proven.
|
||||
pub fn transcript(&self) -> Option<&PartialTranscript> {
|
||||
self.transcript.as_ref()
|
||||
/// Returns the ranges of the transcript to be revealed.
|
||||
pub fn reveal(&self) -> Option<&(RangeSet<usize>, RangeSet<usize>)> {
|
||||
self.reveal.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the transcript commitment configuration.
|
||||
@@ -57,8 +60,7 @@ impl ProveConfig {
|
||||
pub struct ProveConfigBuilder<'a> {
|
||||
transcript: &'a Transcript,
|
||||
server_identity: bool,
|
||||
reveal_sent: Idx,
|
||||
reveal_recv: Idx,
|
||||
reveal: Option<(RangeSet<usize>, RangeSet<usize>)>,
|
||||
transcript_commit: Option<TranscriptCommitConfig>,
|
||||
}
|
||||
|
||||
@@ -68,8 +70,7 @@ impl<'a> ProveConfigBuilder<'a> {
|
||||
Self {
|
||||
transcript,
|
||||
server_identity: false,
|
||||
reveal_sent: Idx::default(),
|
||||
reveal_recv: Idx::default(),
|
||||
reveal: None,
|
||||
transcript_commit: None,
|
||||
}
|
||||
}
|
||||
@@ -92,22 +93,24 @@ impl<'a> ProveConfigBuilder<'a> {
|
||||
direction: Direction,
|
||||
ranges: &dyn ToRangeSet<usize>,
|
||||
) -> Result<&mut Self, ProveConfigBuilderError> {
|
||||
let idx = Idx::new(ranges.to_range_set());
|
||||
let idx = ranges.to_range_set();
|
||||
|
||||
if idx.end() > self.transcript.len_of_direction(direction) {
|
||||
if idx.end().unwrap_or(0) > self.transcript.len_of_direction(direction) {
|
||||
return Err(ProveConfigBuilderError(
|
||||
ProveConfigBuilderErrorRepr::IndexOutOfBounds {
|
||||
direction,
|
||||
actual: idx.end(),
|
||||
actual: idx.end().unwrap_or(0),
|
||||
len: self.transcript.len_of_direction(direction),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let (sent, recv) = self.reveal.get_or_insert_default();
|
||||
match direction {
|
||||
Direction::Sent => self.reveal_sent.union_mut(&idx),
|
||||
Direction::Received => self.reveal_recv.union_mut(&idx),
|
||||
Direction::Sent => sent.union_mut(&idx),
|
||||
Direction::Received => recv.union_mut(&idx),
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
@@ -129,18 +132,9 @@ impl<'a> ProveConfigBuilder<'a> {
|
||||
|
||||
/// Builds the configuration.
|
||||
pub fn build(self) -> Result<ProveConfig, ProveConfigBuilderError> {
|
||||
let transcript = if !self.reveal_sent.is_empty() || !self.reveal_recv.is_empty() {
|
||||
Some(
|
||||
self.transcript
|
||||
.to_partial(self.reveal_sent, self.reveal_recv),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ProveConfig {
|
||||
server_identity: self.server_identity,
|
||||
transcript,
|
||||
reveal: self.reveal,
|
||||
transcript_commit: self.transcript_commit,
|
||||
})
|
||||
}
|
||||
@@ -162,7 +156,7 @@ enum ProveConfigBuilderErrorRepr {
|
||||
}
|
||||
|
||||
/// Configuration to verify information from the prover.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct VerifyConfig {}
|
||||
|
||||
impl VerifyConfig {
|
||||
@@ -200,8 +194,8 @@ enum VerifyConfigBuilderErrorRepr {}
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProvePayload {
|
||||
/// Server identity data.
|
||||
pub server_identity: Option<(ServerName, ServerCertData)>,
|
||||
/// Handshake data.
|
||||
pub handshake: Option<(ServerName, HandshakeData)>,
|
||||
/// Transcript data.
|
||||
pub transcript: Option<PartialTranscript>,
|
||||
/// Transcript commitment configuration.
|
||||
@@ -209,6 +203,7 @@ pub struct ProvePayload {
|
||||
}
|
||||
|
||||
/// Prover output.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ProverOutput {
|
||||
/// Transcript commitments.
|
||||
pub transcript_commitments: Vec<TranscriptCommitment>,
|
||||
@@ -219,6 +214,7 @@ pub struct ProverOutput {
|
||||
opaque_debug::implement!(ProverOutput);
|
||||
|
||||
/// Verifier output.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VerifierOutput {
|
||||
/// Server identity.
|
||||
pub server_name: Option<ServerName>,
|
||||
|
||||
@@ -26,7 +26,7 @@ mod tls;
|
||||
|
||||
use std::{fmt, ops::Range};
|
||||
|
||||
use rangeset::{Difference, IndexRanges, RangeSet, Subset, ToRangeSet, Union, UnionMut};
|
||||
use rangeset::{Difference, IndexRanges, RangeSet, Union};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::connection::TranscriptLength;
|
||||
@@ -39,6 +39,7 @@ pub use proof::{
|
||||
TranscriptProof, TranscriptProofBuilder, TranscriptProofBuilderError, TranscriptProofError,
|
||||
};
|
||||
pub use tls::{Record, TlsTranscript};
|
||||
pub use tls_core::msgs::enums::ContentType;
|
||||
|
||||
/// A transcript contains the plaintext of all application data communicated
|
||||
/// between the Prover and the Server.
|
||||
@@ -95,18 +96,18 @@ impl Transcript {
|
||||
|
||||
/// Returns the subsequence of the transcript with the provided index,
|
||||
/// returning `None` if the index is out of bounds.
|
||||
pub fn get(&self, direction: Direction, idx: &Idx) -> Option<Subsequence> {
|
||||
pub fn get(&self, direction: Direction, idx: &RangeSet<usize>) -> Option<Subsequence> {
|
||||
let data = match direction {
|
||||
Direction::Sent => &self.sent,
|
||||
Direction::Received => &self.received,
|
||||
};
|
||||
|
||||
if idx.end() > data.len() {
|
||||
if idx.end().unwrap_or(0) > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Subsequence::new(idx.clone(), data.index_ranges(&idx.0))
|
||||
Subsequence::new(idx.clone(), data.index_ranges(idx))
|
||||
.expect("data is same length as index"),
|
||||
)
|
||||
}
|
||||
@@ -121,7 +122,11 @@ impl Transcript {
|
||||
///
|
||||
/// * `sent_idx` - The indices of the sent data to include.
|
||||
/// * `recv_idx` - The indices of the received data to include.
|
||||
pub fn to_partial(&self, sent_idx: Idx, recv_idx: Idx) -> PartialTranscript {
|
||||
pub fn to_partial(
|
||||
&self,
|
||||
sent_idx: RangeSet<usize>,
|
||||
recv_idx: RangeSet<usize>,
|
||||
) -> PartialTranscript {
|
||||
let mut sent = vec![0; self.sent.len()];
|
||||
let mut received = vec![0; self.received.len()];
|
||||
|
||||
@@ -156,9 +161,9 @@ pub struct PartialTranscript {
|
||||
/// Data received by the Prover from the Server.
|
||||
received: Vec<u8>,
|
||||
/// Index of `sent` which have been authenticated.
|
||||
sent_authed_idx: Idx,
|
||||
sent_authed_idx: RangeSet<usize>,
|
||||
/// Index of `received` which have been authenticated.
|
||||
received_authed_idx: Idx,
|
||||
received_authed_idx: RangeSet<usize>,
|
||||
}
|
||||
|
||||
/// `PartialTranscript` in a compressed form.
|
||||
@@ -170,9 +175,9 @@ pub struct CompressedPartialTranscript {
|
||||
/// Received data which has been authenticated.
|
||||
received_authed: Vec<u8>,
|
||||
/// Index of `sent_authed`.
|
||||
sent_idx: Idx,
|
||||
sent_idx: RangeSet<usize>,
|
||||
/// Index of `received_authed`.
|
||||
recv_idx: Idx,
|
||||
recv_idx: RangeSet<usize>,
|
||||
/// Total bytelength of sent data in the original partial transcript.
|
||||
sent_total: usize,
|
||||
/// Total bytelength of received data in the original partial transcript.
|
||||
@@ -184,10 +189,10 @@ impl From<PartialTranscript> for CompressedPartialTranscript {
|
||||
Self {
|
||||
sent_authed: uncompressed
|
||||
.sent
|
||||
.index_ranges(&uncompressed.sent_authed_idx.0),
|
||||
.index_ranges(&uncompressed.sent_authed_idx),
|
||||
received_authed: uncompressed
|
||||
.received
|
||||
.index_ranges(&uncompressed.received_authed_idx.0),
|
||||
.index_ranges(&uncompressed.received_authed_idx),
|
||||
sent_idx: uncompressed.sent_authed_idx,
|
||||
recv_idx: uncompressed.received_authed_idx,
|
||||
sent_total: uncompressed.sent.len(),
|
||||
@@ -237,8 +242,8 @@ impl PartialTranscript {
|
||||
Self {
|
||||
sent: vec![0; sent_len],
|
||||
received: vec![0; received_len],
|
||||
sent_authed_idx: Idx::default(),
|
||||
received_authed_idx: Idx::default(),
|
||||
sent_authed_idx: RangeSet::default(),
|
||||
received_authed_idx: RangeSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,10 +264,10 @@ impl PartialTranscript {
|
||||
}
|
||||
|
||||
/// Returns whether the index is in bounds of the transcript.
|
||||
pub fn contains(&self, direction: Direction, idx: &Idx) -> bool {
|
||||
pub fn contains(&self, direction: Direction, idx: &RangeSet<usize>) -> bool {
|
||||
match direction {
|
||||
Direction::Sent => idx.end() <= self.sent.len(),
|
||||
Direction::Received => idx.end() <= self.received.len(),
|
||||
Direction::Sent => idx.end().unwrap_or(0) <= self.sent.len(),
|
||||
Direction::Received => idx.end().unwrap_or(0) <= self.received.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,23 +294,23 @@ impl PartialTranscript {
|
||||
}
|
||||
|
||||
/// Returns the index of sent data which have been authenticated.
|
||||
pub fn sent_authed(&self) -> &Idx {
|
||||
pub fn sent_authed(&self) -> &RangeSet<usize> {
|
||||
&self.sent_authed_idx
|
||||
}
|
||||
|
||||
/// Returns the index of received data which have been authenticated.
|
||||
pub fn received_authed(&self) -> &Idx {
|
||||
pub fn received_authed(&self) -> &RangeSet<usize> {
|
||||
&self.received_authed_idx
|
||||
}
|
||||
|
||||
/// Returns the index of sent data which haven't been authenticated.
|
||||
pub fn sent_unauthed(&self) -> Idx {
|
||||
Idx(RangeSet::from(0..self.sent.len()).difference(&self.sent_authed_idx.0))
|
||||
pub fn sent_unauthed(&self) -> RangeSet<usize> {
|
||||
(0..self.sent.len()).difference(&self.sent_authed_idx)
|
||||
}
|
||||
|
||||
/// Returns the index of received data which haven't been authenticated.
|
||||
pub fn received_unauthed(&self) -> Idx {
|
||||
Idx(RangeSet::from(0..self.received.len()).difference(&self.received_authed_idx.0))
|
||||
pub fn received_unauthed(&self) -> RangeSet<usize> {
|
||||
(0..self.received.len()).difference(&self.received_authed_idx)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the authenticated data in the transcript.
|
||||
@@ -315,7 +320,7 @@ impl PartialTranscript {
|
||||
Direction::Received => (&self.received, &self.received_authed_idx),
|
||||
};
|
||||
|
||||
authed.0.iter().map(|i| data[i])
|
||||
authed.iter().map(|i| data[i])
|
||||
}
|
||||
|
||||
/// Unions the authenticated data of this transcript with another.
|
||||
@@ -337,8 +342,7 @@ impl PartialTranscript {
|
||||
|
||||
for range in other
|
||||
.sent_authed_idx
|
||||
.0
|
||||
.difference(&self.sent_authed_idx.0)
|
||||
.difference(&self.sent_authed_idx)
|
||||
.iter_ranges()
|
||||
{
|
||||
self.sent[range.clone()].copy_from_slice(&other.sent[range]);
|
||||
@@ -346,8 +350,7 @@ impl PartialTranscript {
|
||||
|
||||
for range in other
|
||||
.received_authed_idx
|
||||
.0
|
||||
.difference(&self.received_authed_idx.0)
|
||||
.difference(&self.received_authed_idx)
|
||||
.iter_ranges()
|
||||
{
|
||||
self.received[range.clone()].copy_from_slice(&other.received[range]);
|
||||
@@ -399,12 +402,12 @@ impl PartialTranscript {
|
||||
pub fn set_unauthed_range(&mut self, value: u8, direction: Direction, range: Range<usize>) {
|
||||
match direction {
|
||||
Direction::Sent => {
|
||||
for range in range.difference(&self.sent_authed_idx.0).iter_ranges() {
|
||||
for range in range.difference(&self.sent_authed_idx).iter_ranges() {
|
||||
self.sent[range].fill(value);
|
||||
}
|
||||
}
|
||||
Direction::Received => {
|
||||
for range in range.difference(&self.received_authed_idx.0).iter_ranges() {
|
||||
for range in range.difference(&self.received_authed_idx).iter_ranges() {
|
||||
self.received[range].fill(value);
|
||||
}
|
||||
}
|
||||
@@ -433,130 +436,19 @@ impl fmt::Display for Direction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transcript index.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Idx(RangeSet<usize>);
|
||||
|
||||
impl Idx {
|
||||
/// Creates a new index builder.
|
||||
pub fn builder() -> IdxBuilder {
|
||||
IdxBuilder::default()
|
||||
}
|
||||
|
||||
/// Creates an empty index.
|
||||
pub fn empty() -> Self {
|
||||
Self(RangeSet::default())
|
||||
}
|
||||
|
||||
/// Creates a new transcript index.
|
||||
pub fn new(ranges: impl Into<RangeSet<usize>>) -> Self {
|
||||
Self(ranges.into())
|
||||
}
|
||||
|
||||
/// Returns the start of the index.
|
||||
pub fn start(&self) -> usize {
|
||||
self.0.min().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the end of the index, non-inclusive.
|
||||
pub fn end(&self) -> usize {
|
||||
self.0.end().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the values in the index.
|
||||
pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the ranges of the index.
|
||||
pub fn iter_ranges(&self) -> impl Iterator<Item = Range<usize>> + '_ {
|
||||
self.0.iter_ranges()
|
||||
}
|
||||
|
||||
/// Returns the number of values in the index.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns whether the index is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of disjoint ranges in the index.
|
||||
pub fn count(&self) -> usize {
|
||||
self.0.len_ranges()
|
||||
}
|
||||
|
||||
pub(crate) fn as_range_set(&self) -> &RangeSet<usize> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns the union of this index with another.
|
||||
pub(crate) fn union(&self, other: &Idx) -> Idx {
|
||||
Idx(self.0.union(&other.0))
|
||||
}
|
||||
|
||||
/// Unions this index with another.
|
||||
pub(crate) fn union_mut(&mut self, other: &Idx) {
|
||||
self.0.union_mut(&other.0);
|
||||
}
|
||||
|
||||
/// Returns the difference between `self` and `other`.
|
||||
pub(crate) fn difference(&self, other: &Idx) -> Idx {
|
||||
Idx(self.0.difference(&other.0))
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is a subset of `other`.
|
||||
pub(crate) fn is_subset(&self, other: &Idx) -> bool {
|
||||
self.0.is_subset(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Idx {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Idx([")?;
|
||||
let count = self.0.len_ranges();
|
||||
for (i, range) in self.0.iter_ranges().enumerate() {
|
||||
write!(f, "{}..{}", range.start, range.end)?;
|
||||
if i < count - 1 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
}
|
||||
f.write_str("])")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for [`Idx`].
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdxBuilder(RangeSet<usize>);
|
||||
|
||||
impl IdxBuilder {
|
||||
/// Unions ranges.
|
||||
pub fn union(self, ranges: &dyn ToRangeSet<usize>) -> Self {
|
||||
IdxBuilder(self.0.union(&ranges.to_range_set()))
|
||||
}
|
||||
|
||||
/// Builds the index.
|
||||
pub fn build(self) -> Idx {
|
||||
Idx(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transcript subsequence.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "validation::SubsequenceUnchecked")]
|
||||
pub struct Subsequence {
|
||||
/// Index of the subsequence.
|
||||
idx: Idx,
|
||||
idx: RangeSet<usize>,
|
||||
/// Data of the subsequence.
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Subsequence {
|
||||
/// Creates a new subsequence.
|
||||
pub fn new(idx: Idx, data: Vec<u8>) -> Result<Self, InvalidSubsequence> {
|
||||
pub fn new(idx: RangeSet<usize>, data: Vec<u8>) -> Result<Self, InvalidSubsequence> {
|
||||
if idx.len() != data.len() {
|
||||
return Err(InvalidSubsequence(
|
||||
"index length does not match data length",
|
||||
@@ -567,7 +459,7 @@ impl Subsequence {
|
||||
}
|
||||
|
||||
/// Returns the index of the subsequence.
|
||||
pub fn index(&self) -> &Idx {
|
||||
pub fn index(&self) -> &RangeSet<usize> {
|
||||
&self.idx
|
||||
}
|
||||
|
||||
@@ -583,7 +475,7 @@ impl Subsequence {
|
||||
}
|
||||
|
||||
/// Returns the inner parts of the subsequence.
|
||||
pub fn into_parts(self) -> (Idx, Vec<u8>) {
|
||||
pub fn into_parts(self) -> (RangeSet<usize>, Vec<u8>) {
|
||||
(self.idx, self.data)
|
||||
}
|
||||
|
||||
@@ -611,7 +503,7 @@ mod validation {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct SubsequenceUnchecked {
|
||||
idx: Idx,
|
||||
idx: RangeSet<usize>,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
@@ -633,8 +525,8 @@ mod validation {
|
||||
pub(super) struct CompressedPartialTranscriptUnchecked {
|
||||
sent_authed: Vec<u8>,
|
||||
received_authed: Vec<u8>,
|
||||
sent_idx: Idx,
|
||||
recv_idx: Idx,
|
||||
sent_idx: RangeSet<usize>,
|
||||
recv_idx: RangeSet<usize>,
|
||||
sent_total: usize,
|
||||
recv_total: usize,
|
||||
}
|
||||
@@ -651,8 +543,8 @@ mod validation {
|
||||
));
|
||||
}
|
||||
|
||||
if unchecked.sent_idx.end() > unchecked.sent_total
|
||||
|| unchecked.recv_idx.end() > unchecked.recv_total
|
||||
if unchecked.sent_idx.end().unwrap_or(0) > unchecked.sent_total
|
||||
|| unchecked.recv_idx.end().unwrap_or(0) > unchecked.recv_total
|
||||
{
|
||||
return Err(InvalidCompressedPartialTranscript(
|
||||
"ranges are not in bounds of the data",
|
||||
@@ -681,8 +573,8 @@ mod validation {
|
||||
CompressedPartialTranscriptUnchecked {
|
||||
received_authed: vec![1, 2, 3, 11, 12, 13],
|
||||
sent_authed: vec![4, 5, 6, 14, 15, 16],
|
||||
recv_idx: Idx(RangeSet::new(&[1..4, 11..14])),
|
||||
sent_idx: Idx(RangeSet::new(&[4..7, 14..17])),
|
||||
recv_idx: RangeSet::from([1..4, 11..14]),
|
||||
sent_idx: RangeSet::from([4..7, 14..17]),
|
||||
sent_total: 20,
|
||||
recv_total: 20,
|
||||
}
|
||||
@@ -721,7 +613,6 @@ mod validation {
|
||||
// Change the total to be less than the last range's end bound.
|
||||
let end = partial_transcript
|
||||
.sent_idx
|
||||
.0
|
||||
.iter_ranges()
|
||||
.next_back()
|
||||
.unwrap()
|
||||
@@ -753,31 +644,25 @@ mod tests {
|
||||
|
||||
#[fixture]
|
||||
fn partial_transcript() -> PartialTranscript {
|
||||
transcript().to_partial(
|
||||
Idx::new(RangeSet::new(&[1..4, 6..9])),
|
||||
Idx::new(RangeSet::new(&[2..5, 7..10])),
|
||||
)
|
||||
transcript().to_partial(RangeSet::from([1..4, 6..9]), RangeSet::from([2..5, 7..10]))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_transcript_get_subsequence(transcript: Transcript) {
|
||||
let subseq = transcript
|
||||
.get(Direction::Received, &Idx(RangeSet::from([0..4, 7..10])))
|
||||
.get(Direction::Received, &RangeSet::from([0..4, 7..10]))
|
||||
.unwrap();
|
||||
assert_eq!(subseq.data, vec![0, 1, 2, 3, 7, 8, 9]);
|
||||
|
||||
let subseq = transcript
|
||||
.get(Direction::Sent, &Idx(RangeSet::from([0..4, 9..12])))
|
||||
.get(Direction::Sent, &RangeSet::from([0..4, 9..12]))
|
||||
.unwrap();
|
||||
assert_eq!(subseq.data, vec![0, 1, 2, 3, 9, 10, 11]);
|
||||
|
||||
let subseq = transcript.get(
|
||||
Direction::Received,
|
||||
&Idx(RangeSet::from([0..4, 7..10, 11..13])),
|
||||
);
|
||||
let subseq = transcript.get(Direction::Received, &RangeSet::from([0..4, 7..10, 11..13]));
|
||||
assert_eq!(subseq, None);
|
||||
|
||||
let subseq = transcript.get(Direction::Sent, &Idx(RangeSet::from([0..4, 7..10, 11..13])));
|
||||
let subseq = transcript.get(Direction::Sent, &RangeSet::from([0..4, 7..10, 11..13]));
|
||||
assert_eq!(subseq, None);
|
||||
}
|
||||
|
||||
@@ -790,7 +675,7 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
fn test_transcript_to_partial_success(transcript: Transcript) {
|
||||
let partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
let partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
assert_eq!(partial.sent_unsafe(), [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
assert_eq!(
|
||||
partial.received_unsafe(),
|
||||
@@ -801,29 +686,30 @@ mod tests {
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_transcript_to_partial_failure(transcript: Transcript) {
|
||||
let _ = transcript.to_partial(Idx::new(0..14), Idx::new(3..7));
|
||||
let _ = transcript.to_partial(RangeSet::from(0..14), RangeSet::from(3..7));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_contains(transcript: Transcript) {
|
||||
let partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
assert!(partial.contains(Direction::Sent, &Idx::new([0..5, 7..10])));
|
||||
assert!(!partial.contains(Direction::Received, &Idx::new([4..6, 7..13])))
|
||||
let partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
assert!(partial.contains(Direction::Sent, &RangeSet::from([0..5, 7..10])));
|
||||
assert!(!partial.contains(Direction::Received, &RangeSet::from([4..6, 7..13])))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_unauthed(transcript: Transcript) {
|
||||
let partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
assert_eq!(partial.sent_unauthed(), Idx::new(2..12));
|
||||
assert_eq!(partial.received_unauthed(), Idx::new([0..3, 7..12]));
|
||||
let partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
assert_eq!(partial.sent_unauthed(), RangeSet::from(2..12));
|
||||
assert_eq!(partial.received_unauthed(), RangeSet::from([0..3, 7..12]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_union_success(transcript: Transcript) {
|
||||
// Non overlapping ranges.
|
||||
let mut simple_partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
let mut simple_partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
|
||||
let other_simple_partial = transcript.to_partial(Idx::new(3..5), Idx::new(1..2));
|
||||
let other_simple_partial =
|
||||
transcript.to_partial(RangeSet::from(3..5), RangeSet::from(1..2));
|
||||
|
||||
simple_partial.union_transcript(&other_simple_partial);
|
||||
|
||||
@@ -835,12 +721,16 @@ mod tests {
|
||||
simple_partial.received_unsafe(),
|
||||
[0, 1, 0, 3, 4, 5, 6, 0, 0, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(simple_partial.sent_authed(), &Idx::new([0..2, 3..5]));
|
||||
assert_eq!(simple_partial.received_authed(), &Idx::new([1..2, 3..7]));
|
||||
assert_eq!(simple_partial.sent_authed(), &RangeSet::from([0..2, 3..5]));
|
||||
assert_eq!(
|
||||
simple_partial.received_authed(),
|
||||
&RangeSet::from([1..2, 3..7])
|
||||
);
|
||||
|
||||
// Overwrite with another partial transcript.
|
||||
|
||||
let another_simple_partial = transcript.to_partial(Idx::new(1..4), Idx::new(6..9));
|
||||
let another_simple_partial =
|
||||
transcript.to_partial(RangeSet::from(1..4), RangeSet::from(6..9));
|
||||
|
||||
simple_partial.union_transcript(&another_simple_partial);
|
||||
|
||||
@@ -852,13 +742,17 @@ mod tests {
|
||||
simple_partial.received_unsafe(),
|
||||
[0, 1, 0, 3, 4, 5, 6, 7, 8, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(simple_partial.sent_authed(), &Idx::new(0..5));
|
||||
assert_eq!(simple_partial.received_authed(), &Idx::new([1..2, 3..9]));
|
||||
assert_eq!(simple_partial.sent_authed(), &RangeSet::from(0..5));
|
||||
assert_eq!(
|
||||
simple_partial.received_authed(),
|
||||
&RangeSet::from([1..2, 3..9])
|
||||
);
|
||||
|
||||
// Overlapping ranges.
|
||||
let mut overlap_partial = transcript.to_partial(Idx::new(4..6), Idx::new(3..7));
|
||||
let mut overlap_partial = transcript.to_partial(RangeSet::from(4..6), RangeSet::from(3..7));
|
||||
|
||||
let other_overlap_partial = transcript.to_partial(Idx::new(3..5), Idx::new(5..9));
|
||||
let other_overlap_partial =
|
||||
transcript.to_partial(RangeSet::from(3..5), RangeSet::from(5..9));
|
||||
|
||||
overlap_partial.union_transcript(&other_overlap_partial);
|
||||
|
||||
@@ -870,13 +764,16 @@ mod tests {
|
||||
overlap_partial.received_unsafe(),
|
||||
[0, 0, 0, 3, 4, 5, 6, 7, 8, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(overlap_partial.sent_authed(), &Idx::new([3..5, 4..6]));
|
||||
assert_eq!(overlap_partial.received_authed(), &Idx::new([3..7, 5..9]));
|
||||
assert_eq!(overlap_partial.sent_authed(), &RangeSet::from([3..5, 4..6]));
|
||||
assert_eq!(
|
||||
overlap_partial.received_authed(),
|
||||
&RangeSet::from([3..7, 5..9])
|
||||
);
|
||||
|
||||
// Equal ranges.
|
||||
let mut equal_partial = transcript.to_partial(Idx::new(4..6), Idx::new(3..7));
|
||||
let mut equal_partial = transcript.to_partial(RangeSet::from(4..6), RangeSet::from(3..7));
|
||||
|
||||
let other_equal_partial = transcript.to_partial(Idx::new(4..6), Idx::new(3..7));
|
||||
let other_equal_partial = transcript.to_partial(RangeSet::from(4..6), RangeSet::from(3..7));
|
||||
|
||||
equal_partial.union_transcript(&other_equal_partial);
|
||||
|
||||
@@ -888,13 +785,15 @@ mod tests {
|
||||
equal_partial.received_unsafe(),
|
||||
[0, 0, 0, 3, 4, 5, 6, 0, 0, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(equal_partial.sent_authed(), &Idx::new(4..6));
|
||||
assert_eq!(equal_partial.received_authed(), &Idx::new(3..7));
|
||||
assert_eq!(equal_partial.sent_authed(), &RangeSet::from(4..6));
|
||||
assert_eq!(equal_partial.received_authed(), &RangeSet::from(3..7));
|
||||
|
||||
// Subset ranges.
|
||||
let mut subset_partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let mut subset_partial =
|
||||
transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
|
||||
let other_subset_partial = transcript.to_partial(Idx::new(6..9), Idx::new(5..6));
|
||||
let other_subset_partial =
|
||||
transcript.to_partial(RangeSet::from(6..9), RangeSet::from(5..6));
|
||||
|
||||
subset_partial.union_transcript(&other_subset_partial);
|
||||
|
||||
@@ -906,30 +805,32 @@ mod tests {
|
||||
subset_partial.received_unsafe(),
|
||||
[0, 0, 0, 3, 4, 5, 6, 7, 8, 9, 10, 0]
|
||||
);
|
||||
assert_eq!(subset_partial.sent_authed(), &Idx::new(4..10));
|
||||
assert_eq!(subset_partial.received_authed(), &Idx::new(3..11));
|
||||
assert_eq!(subset_partial.sent_authed(), &RangeSet::from(4..10));
|
||||
assert_eq!(subset_partial.received_authed(), &RangeSet::from(3..11));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_partial_transcript_union_failure(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
|
||||
let other_transcript = Transcript::new(
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
);
|
||||
|
||||
let other_partial = other_transcript.to_partial(Idx::new(6..9), Idx::new(5..6));
|
||||
let other_partial = other_transcript.to_partial(RangeSet::from(6..9), RangeSet::from(5..6));
|
||||
|
||||
partial.union_transcript(&other_partial);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_union_subseq_success(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let sent_seq = Subsequence::new(Idx::new([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let recv_seq = Subsequence::new(Idx::new([0..4, 5..7]), [0, 1, 2, 3, 5, 6].into()).unwrap();
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
let sent_seq =
|
||||
Subsequence::new(RangeSet::from([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let recv_seq =
|
||||
Subsequence::new(RangeSet::from([0..4, 5..7]), [0, 1, 2, 3, 5, 6].into()).unwrap();
|
||||
|
||||
partial.union_subsequence(Direction::Sent, &sent_seq);
|
||||
partial.union_subsequence(Direction::Received, &recv_seq);
|
||||
@@ -939,30 +840,31 @@ mod tests {
|
||||
partial.received_unsafe(),
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0]
|
||||
);
|
||||
assert_eq!(partial.sent_authed(), &Idx::new([0..3, 4..10]));
|
||||
assert_eq!(partial.received_authed(), &Idx::new(0..11));
|
||||
assert_eq!(partial.sent_authed(), &RangeSet::from([0..3, 4..10]));
|
||||
assert_eq!(partial.received_authed(), &RangeSet::from(0..11));
|
||||
|
||||
// Overwrite with another subseq.
|
||||
let other_sent_seq = Subsequence::new(Idx::new(0..3), [3, 2, 1].into()).unwrap();
|
||||
let other_sent_seq = Subsequence::new(RangeSet::from(0..3), [3, 2, 1].into()).unwrap();
|
||||
|
||||
partial.union_subsequence(Direction::Sent, &other_sent_seq);
|
||||
assert_eq!(partial.sent_unsafe(), [3, 2, 1, 0, 4, 5, 6, 7, 8, 9, 0, 0]);
|
||||
assert_eq!(partial.sent_authed(), &Idx::new([0..3, 4..10]));
|
||||
assert_eq!(partial.sent_authed(), &RangeSet::from([0..3, 4..10]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_partial_transcript_union_subseq_failure(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
|
||||
let sent_seq = Subsequence::new(Idx::new([0..3, 13..15]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let sent_seq =
|
||||
Subsequence::new(RangeSet::from([0..3, 13..15]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
|
||||
partial.union_subsequence(Direction::Sent, &sent_seq);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_set_unauthed_range(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..7));
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..7));
|
||||
|
||||
partial.set_unauthed_range(7, Direction::Sent, 2..5);
|
||||
partial.set_unauthed_range(5, Direction::Sent, 0..2);
|
||||
@@ -979,13 +881,13 @@ mod tests {
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_subsequence_new_invalid_len() {
|
||||
let _ = Subsequence::new(Idx::new([0..3, 5..8]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let _ = Subsequence::new(RangeSet::from([0..3, 5..8]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_subsequence_copy_to_invalid_len() {
|
||||
let seq = Subsequence::new(Idx::new([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let seq = Subsequence::new(RangeSet::from([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
|
||||
let mut data: [u8; 3] = [0, 1, 2];
|
||||
seq.copy_to(&mut data);
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
transcript::{
|
||||
encoding::{EncodingCommitment, EncodingTree},
|
||||
hash::{PlaintextHash, PlaintextHashSecret},
|
||||
Direction, Idx, Transcript,
|
||||
Direction, RangeSet, Transcript,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -66,17 +66,17 @@ pub enum TranscriptSecret {
|
||||
}
|
||||
|
||||
/// Configuration for transcript commitments.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TranscriptCommitConfig {
|
||||
encoding_hash_alg: HashAlgId,
|
||||
has_encoding: bool,
|
||||
has_hash: bool,
|
||||
commits: Vec<((Direction, Idx), TranscriptCommitmentKind)>,
|
||||
commits: Vec<((Direction, RangeSet<usize>), TranscriptCommitmentKind)>,
|
||||
}
|
||||
|
||||
impl TranscriptCommitConfig {
|
||||
/// Creates a new commit config builder.
|
||||
pub fn builder(transcript: &Transcript) -> TranscriptCommitConfigBuilder {
|
||||
pub fn builder(transcript: &Transcript) -> TranscriptCommitConfigBuilder<'_> {
|
||||
TranscriptCommitConfigBuilder::new(transcript)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ impl TranscriptCommitConfig {
|
||||
}
|
||||
|
||||
/// Returns an iterator over the encoding commitment indices.
|
||||
pub fn iter_encoding(&self) -> impl Iterator<Item = &(Direction, Idx)> {
|
||||
pub fn iter_encoding(&self) -> impl Iterator<Item = &(Direction, RangeSet<usize>)> {
|
||||
self.commits.iter().filter_map(|(idx, kind)| match kind {
|
||||
TranscriptCommitmentKind::Encoding => Some(idx),
|
||||
_ => None,
|
||||
@@ -104,7 +104,7 @@ impl TranscriptCommitConfig {
|
||||
}
|
||||
|
||||
/// Returns an iterator over the hash commitment indices.
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = (&(Direction, Idx), &HashAlgId)> {
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = (&(Direction, RangeSet<usize>), &HashAlgId)> {
|
||||
self.commits.iter().filter_map(|(idx, kind)| match kind {
|
||||
TranscriptCommitmentKind::Hash { alg } => Some((idx, alg)),
|
||||
_ => None,
|
||||
@@ -134,7 +134,7 @@ pub struct TranscriptCommitConfigBuilder<'a> {
|
||||
has_encoding: bool,
|
||||
has_hash: bool,
|
||||
default_kind: TranscriptCommitmentKind,
|
||||
commits: HashSet<((Direction, Idx), TranscriptCommitmentKind)>,
|
||||
commits: HashSet<((Direction, RangeSet<usize>), TranscriptCommitmentKind)>,
|
||||
}
|
||||
|
||||
impl<'a> TranscriptCommitConfigBuilder<'a> {
|
||||
@@ -175,15 +175,15 @@ impl<'a> TranscriptCommitConfigBuilder<'a> {
|
||||
direction: Direction,
|
||||
kind: TranscriptCommitmentKind,
|
||||
) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
|
||||
let idx = Idx::new(ranges.to_range_set());
|
||||
let idx = ranges.to_range_set();
|
||||
|
||||
if idx.end() > self.transcript.len_of_direction(direction) {
|
||||
if idx.end().unwrap_or(0) > self.transcript.len_of_direction(direction) {
|
||||
return Err(TranscriptCommitConfigBuilderError::new(
|
||||
ErrorKind::Index,
|
||||
format!(
|
||||
"range is out of bounds of the transcript ({}): {} > {}",
|
||||
direction,
|
||||
idx.end(),
|
||||
idx.end().unwrap_or(0),
|
||||
self.transcript.len_of_direction(direction)
|
||||
),
|
||||
));
|
||||
@@ -290,7 +290,7 @@ impl fmt::Display for TranscriptCommitConfigBuilderError {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TranscriptCommitRequest {
|
||||
encoding: bool,
|
||||
hash: Vec<(Direction, Idx, HashAlgId)>,
|
||||
hash: Vec<(Direction, RangeSet<usize>, HashAlgId)>,
|
||||
}
|
||||
|
||||
impl TranscriptCommitRequest {
|
||||
@@ -305,7 +305,7 @@ impl TranscriptCommitRequest {
|
||||
}
|
||||
|
||||
/// Returns an iterator over the hash commitments.
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = &(Direction, Idx, HashAlgId)> {
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = &(Direction, RangeSet<usize>, HashAlgId)> {
|
||||
self.hash.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
transcript::{
|
||||
commit::MAX_TOTAL_COMMITTED_DATA,
|
||||
encoding::{new_encoder, Encoder, EncodingCommitment},
|
||||
Direction, Idx,
|
||||
Direction,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub(super) struct Opening {
|
||||
pub(super) direction: Direction,
|
||||
pub(super) idx: Idx,
|
||||
pub(super) idx: RangeSet<usize>,
|
||||
pub(super) blinder: Blinder,
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl EncodingProof {
|
||||
commitment: &EncodingCommitment,
|
||||
sent: &[u8],
|
||||
recv: &[u8],
|
||||
) -> Result<(Idx, Idx), EncodingProofError> {
|
||||
) -> Result<(RangeSet<usize>, RangeSet<usize>), EncodingProofError> {
|
||||
let hasher = provider.get(&commitment.root.alg)?;
|
||||
|
||||
let encoder = new_encoder(&commitment.secret);
|
||||
@@ -89,13 +89,13 @@ impl EncodingProof {
|
||||
};
|
||||
|
||||
// Make sure the ranges are within the bounds of the transcript.
|
||||
if idx.end() > data.len() {
|
||||
if idx.end().unwrap_or(0) > data.len() {
|
||||
return Err(EncodingProofError::new(
|
||||
ErrorKind::Proof,
|
||||
format!(
|
||||
"index out of bounds of the transcript ({}): {} > {}",
|
||||
direction,
|
||||
idx.end(),
|
||||
idx.end().unwrap_or(0),
|
||||
data.len()
|
||||
),
|
||||
));
|
||||
@@ -111,7 +111,7 @@ impl EncodingProof {
|
||||
// present in the merkle tree.
|
||||
leaves.push((*id, hasher.hash(&expected_leaf)));
|
||||
|
||||
auth.union_mut(idx.as_range_set());
|
||||
auth.union_mut(idx);
|
||||
}
|
||||
|
||||
// Verify that the expected hashes are present in the merkle tree.
|
||||
@@ -121,7 +121,7 @@ impl EncodingProof {
|
||||
// data is authentic.
|
||||
inclusion_proof.verify(hasher, &commitment.root, leaves)?;
|
||||
|
||||
Ok((Idx(auth_sent), Idx(auth_recv)))
|
||||
Ok((auth_sent, auth_recv))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ mod test {
|
||||
hash::Blake3,
|
||||
transcript::{
|
||||
encoding::{EncoderSecret, EncodingTree},
|
||||
Idx, Transcript,
|
||||
Transcript,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,8 +249,8 @@ mod test {
|
||||
fn new_encoding_fixture(secret: EncoderSecret) -> EncodingFixture {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..OK_JSON.len()));
|
||||
|
||||
let provider = encoding_provider(transcript.sent(), transcript.received());
|
||||
let tree = EncodingTree::new(&Blake3::default(), [&idx_0, &idx_1], &provider).unwrap();
|
||||
@@ -317,7 +317,7 @@ mod test {
|
||||
|
||||
let Opening { idx, .. } = proof.openings.values_mut().next().unwrap();
|
||||
|
||||
*idx = Idx::new([0..3, 13..15]);
|
||||
*idx = RangeSet::from([0..3, 13..15]);
|
||||
|
||||
let err = proof
|
||||
.verify_with_provider(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bimap::BiMap;
|
||||
use rangeset::{RangeSet, UnionMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
@@ -11,7 +12,7 @@ use crate::{
|
||||
proof::{EncodingProof, Opening},
|
||||
EncodingProvider,
|
||||
},
|
||||
Direction, Idx,
|
||||
Direction,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,7 +23,7 @@ pub enum EncodingTreeError {
|
||||
#[error("index is out of bounds of the transcript")]
|
||||
OutOfBounds {
|
||||
/// The index.
|
||||
index: Idx,
|
||||
index: RangeSet<usize>,
|
||||
/// The transcript length.
|
||||
transcript_length: usize,
|
||||
},
|
||||
@@ -30,13 +31,13 @@ pub enum EncodingTreeError {
|
||||
#[error("encoding provider is missing an encoding for an index")]
|
||||
MissingEncoding {
|
||||
/// The index which is missing.
|
||||
index: Idx,
|
||||
index: RangeSet<usize>,
|
||||
},
|
||||
/// Index is missing from the tree.
|
||||
#[error("index is missing from the tree")]
|
||||
MissingLeaf {
|
||||
/// The index which is missing.
|
||||
index: Idx,
|
||||
index: RangeSet<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,11 +50,11 @@ pub struct EncodingTree {
|
||||
blinders: Vec<Blinder>,
|
||||
/// Mapping between the index of a leaf and the transcript index it
|
||||
/// corresponds to.
|
||||
idxs: BiMap<usize, (Direction, Idx)>,
|
||||
idxs: BiMap<usize, (Direction, RangeSet<usize>)>,
|
||||
/// Union of all transcript indices in the sent direction.
|
||||
sent_idx: Idx,
|
||||
sent_idx: RangeSet<usize>,
|
||||
/// Union of all transcript indices in the received direction.
|
||||
received_idx: Idx,
|
||||
received_idx: RangeSet<usize>,
|
||||
}
|
||||
|
||||
opaque_debug::implement!(EncodingTree);
|
||||
@@ -68,15 +69,15 @@ impl EncodingTree {
|
||||
/// * `provider` - The encoding provider.
|
||||
pub fn new<'idx>(
|
||||
hasher: &dyn HashAlgorithm,
|
||||
idxs: impl IntoIterator<Item = &'idx (Direction, Idx)>,
|
||||
idxs: impl IntoIterator<Item = &'idx (Direction, RangeSet<usize>)>,
|
||||
provider: &dyn EncodingProvider,
|
||||
) -> Result<Self, EncodingTreeError> {
|
||||
let mut this = Self {
|
||||
tree: MerkleTree::new(hasher.id()),
|
||||
blinders: Vec::new(),
|
||||
idxs: BiMap::new(),
|
||||
sent_idx: Idx::empty(),
|
||||
received_idx: Idx::empty(),
|
||||
sent_idx: RangeSet::default(),
|
||||
received_idx: RangeSet::default(),
|
||||
};
|
||||
|
||||
let mut leaves = Vec::new();
|
||||
@@ -138,7 +139,7 @@ impl EncodingTree {
|
||||
/// * `idxs` - The transcript indices to prove.
|
||||
pub fn proof<'idx>(
|
||||
&self,
|
||||
idxs: impl Iterator<Item = &'idx (Direction, Idx)>,
|
||||
idxs: impl Iterator<Item = &'idx (Direction, RangeSet<usize>)>,
|
||||
) -> Result<EncodingProof, EncodingTreeError> {
|
||||
let mut openings = HashMap::new();
|
||||
for dir_idx in idxs {
|
||||
@@ -171,11 +172,11 @@ impl EncodingTree {
|
||||
}
|
||||
|
||||
/// Returns whether the tree contains the given transcript index.
|
||||
pub fn contains(&self, idx: &(Direction, Idx)) -> bool {
|
||||
pub fn contains(&self, idx: &(Direction, RangeSet<usize>)) -> bool {
|
||||
self.idxs.contains_right(idx)
|
||||
}
|
||||
|
||||
pub(crate) fn idx(&self, direction: Direction) -> &Idx {
|
||||
pub(crate) fn idx(&self, direction: Direction) -> &RangeSet<usize> {
|
||||
match direction {
|
||||
Direction::Sent => &self.sent_idx,
|
||||
Direction::Received => &self.received_idx,
|
||||
@@ -183,7 +184,7 @@ impl EncodingTree {
|
||||
}
|
||||
|
||||
/// Returns the committed transcript indices.
|
||||
pub(crate) fn transcript_indices(&self) -> impl Iterator<Item = &(Direction, Idx)> {
|
||||
pub(crate) fn transcript_indices(&self) -> impl Iterator<Item = &(Direction, RangeSet<usize>)> {
|
||||
self.idxs.right_values()
|
||||
}
|
||||
}
|
||||
@@ -200,7 +201,7 @@ mod tests {
|
||||
|
||||
fn new_tree<'seq>(
|
||||
transcript: &Transcript,
|
||||
idxs: impl Iterator<Item = &'seq (Direction, Idx)>,
|
||||
idxs: impl Iterator<Item = &'seq (Direction, RangeSet<usize>)>,
|
||||
) -> Result<EncodingTree, EncodingTreeError> {
|
||||
let provider = encoding_provider(transcript.sent(), transcript.received());
|
||||
|
||||
@@ -211,8 +212,8 @@ mod tests {
|
||||
fn test_encoding_tree() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..OK_JSON.len()));
|
||||
|
||||
let tree = new_tree(&transcript, [&idx_0, &idx_1].into_iter()).unwrap();
|
||||
|
||||
@@ -243,10 +244,10 @@ mod tests {
|
||||
fn test_encoding_tree_multiple_ranges() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..1));
|
||||
let idx_1 = (Direction::Sent, Idx::new(1..POST_JSON.len()));
|
||||
let idx_2 = (Direction::Received, Idx::new(0..1));
|
||||
let idx_3 = (Direction::Received, Idx::new(1..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..1));
|
||||
let idx_1 = (Direction::Sent, RangeSet::from(1..POST_JSON.len()));
|
||||
let idx_2 = (Direction::Received, RangeSet::from(0..1));
|
||||
let idx_3 = (Direction::Received, RangeSet::from(1..OK_JSON.len()));
|
||||
|
||||
let tree = new_tree(&transcript, [&idx_0, &idx_1, &idx_2, &idx_3].into_iter()).unwrap();
|
||||
|
||||
@@ -273,11 +274,11 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut expected_auth_sent = Idx::default();
|
||||
let mut expected_auth_sent = RangeSet::default();
|
||||
expected_auth_sent.union_mut(&idx_0.1);
|
||||
expected_auth_sent.union_mut(&idx_1.1);
|
||||
|
||||
let mut expected_auth_recv = Idx::default();
|
||||
let mut expected_auth_recv = RangeSet::default();
|
||||
expected_auth_recv.union_mut(&idx_2.1);
|
||||
expected_auth_recv.union_mut(&idx_3.1);
|
||||
|
||||
@@ -289,9 +290,9 @@ mod tests {
|
||||
fn test_encoding_tree_proof_missing_leaf() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..4));
|
||||
let idx_2 = (Direction::Received, Idx::new(4..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..4));
|
||||
let idx_2 = (Direction::Received, RangeSet::from(4..OK_JSON.len()));
|
||||
|
||||
let tree = new_tree(&transcript, [&idx_0, &idx_1].into_iter()).unwrap();
|
||||
|
||||
@@ -305,8 +306,8 @@ mod tests {
|
||||
fn test_encoding_tree_out_of_bounds() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len() + 1));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..OK_JSON.len() + 1));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len() + 1));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..OK_JSON.len() + 1));
|
||||
|
||||
let result = new_tree(&transcript, [&idx_0].into_iter()).unwrap_err();
|
||||
assert!(matches!(result, EncodingTreeError::MissingEncoding { .. }));
|
||||
@@ -321,7 +322,7 @@ mod tests {
|
||||
|
||||
let result = EncodingTree::new(
|
||||
&Blake3::default(),
|
||||
[(Direction::Sent, Idx::new(0..8))].iter(),
|
||||
[(Direction::Sent, RangeSet::from(0..8))].iter(),
|
||||
&provider,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
hash::{Blinder, HashAlgId, HashAlgorithm, TypedHash},
|
||||
transcript::{Direction, Idx},
|
||||
transcript::{Direction, RangeSet},
|
||||
};
|
||||
|
||||
/// Hashes plaintext with a blinder.
|
||||
@@ -23,7 +23,7 @@ pub struct PlaintextHash {
|
||||
/// Direction of the plaintext.
|
||||
pub direction: Direction,
|
||||
/// Index of plaintext.
|
||||
pub idx: Idx,
|
||||
pub idx: RangeSet<usize>,
|
||||
/// The hash of the data.
|
||||
pub hash: TypedHash,
|
||||
}
|
||||
@@ -34,7 +34,7 @@ pub struct PlaintextHashSecret {
|
||||
/// Direction of the plaintext.
|
||||
pub direction: Direction,
|
||||
/// Index of plaintext.
|
||||
pub idx: Idx,
|
||||
pub idx: RangeSet<usize>,
|
||||
/// The algorithm of the hash.
|
||||
pub alg: HashAlgId,
|
||||
/// Blinder for the hash.
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
//! Transcript proofs.
|
||||
|
||||
use rangeset::{Cover, ToRangeSet};
|
||||
use rangeset::{Cover, Difference, Subset, ToRangeSet, UnionMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, fmt};
|
||||
|
||||
use crate::{
|
||||
connection::TranscriptLength,
|
||||
display::FmtRangeSet,
|
||||
hash::{HashAlgId, HashProvider},
|
||||
transcript::{
|
||||
commit::{TranscriptCommitment, TranscriptCommitmentKind},
|
||||
encoding::{EncodingProof, EncodingProofError, EncodingTree},
|
||||
hash::{hash_plaintext, PlaintextHash, PlaintextHashSecret},
|
||||
Direction, Idx, PartialTranscript, Transcript, TranscriptSecret,
|
||||
Direction, PartialTranscript, RangeSet, Transcript, TranscriptSecret,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +22,9 @@ const DEFAULT_COMMITMENT_KINDS: &[TranscriptCommitmentKind] = &[
|
||||
TranscriptCommitmentKind::Hash {
|
||||
alg: HashAlgId::SHA256,
|
||||
},
|
||||
TranscriptCommitmentKind::Hash {
|
||||
alg: HashAlgId::BLAKE3,
|
||||
},
|
||||
TranscriptCommitmentKind::Encoding,
|
||||
];
|
||||
|
||||
@@ -77,8 +81,8 @@ impl TranscriptProof {
|
||||
));
|
||||
}
|
||||
|
||||
let mut total_auth_sent = Idx::default();
|
||||
let mut total_auth_recv = Idx::default();
|
||||
let mut total_auth_sent = RangeSet::default();
|
||||
let mut total_auth_recv = RangeSet::default();
|
||||
|
||||
// Verify encoding proof.
|
||||
if let Some(proof) = self.encoding_proof {
|
||||
@@ -120,7 +124,7 @@ impl TranscriptProof {
|
||||
Direction::Received => (self.transcript.received_unsafe(), &mut total_auth_recv),
|
||||
};
|
||||
|
||||
if idx.end() > plaintext.len() {
|
||||
if idx.end().unwrap_or(0) > plaintext.len() {
|
||||
return Err(TranscriptProofError::new(
|
||||
ErrorKind::Hash,
|
||||
"hash opening index is out of bounds",
|
||||
@@ -215,15 +219,15 @@ impl From<EncodingProofError> for TranscriptProofError {
|
||||
/// Union of ranges to reveal.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct QueryIdx {
|
||||
sent: Idx,
|
||||
recv: Idx,
|
||||
sent: RangeSet<usize>,
|
||||
recv: RangeSet<usize>,
|
||||
}
|
||||
|
||||
impl QueryIdx {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
sent: Idx::empty(),
|
||||
recv: Idx::empty(),
|
||||
sent: RangeSet::default(),
|
||||
recv: RangeSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +235,7 @@ impl QueryIdx {
|
||||
self.sent.is_empty() && self.recv.is_empty()
|
||||
}
|
||||
|
||||
fn union(&mut self, direction: &Direction, other: &Idx) {
|
||||
fn union(&mut self, direction: &Direction, other: &RangeSet<usize>) {
|
||||
match direction {
|
||||
Direction::Sent => self.sent.union_mut(other),
|
||||
Direction::Received => self.recv.union_mut(other),
|
||||
@@ -241,7 +245,12 @@ impl QueryIdx {
|
||||
|
||||
impl std::fmt::Display for QueryIdx {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "sent: {}, received: {}", self.sent, self.recv)
|
||||
write!(
|
||||
f,
|
||||
"sent: {}, received: {}",
|
||||
FmtRangeSet(&self.sent),
|
||||
FmtRangeSet(&self.recv)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +262,8 @@ pub struct TranscriptProofBuilder<'a> {
|
||||
transcript: &'a Transcript,
|
||||
encoding_tree: Option<&'a EncodingTree>,
|
||||
hash_secrets: Vec<&'a PlaintextHashSecret>,
|
||||
committed_sent: Idx,
|
||||
committed_recv: Idx,
|
||||
committed_sent: RangeSet<usize>,
|
||||
committed_recv: RangeSet<usize>,
|
||||
query_idx: QueryIdx,
|
||||
}
|
||||
|
||||
@@ -264,8 +273,8 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
transcript: &'a Transcript,
|
||||
secrets: impl IntoIterator<Item = &'a TranscriptSecret>,
|
||||
) -> Self {
|
||||
let mut committed_sent = Idx::empty();
|
||||
let mut committed_recv = Idx::empty();
|
||||
let mut committed_sent = RangeSet::default();
|
||||
let mut committed_recv = RangeSet::default();
|
||||
|
||||
let mut encoding_tree = None;
|
||||
let mut hash_secrets = Vec::new();
|
||||
@@ -323,15 +332,15 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
ranges: &dyn ToRangeSet<usize>,
|
||||
direction: Direction,
|
||||
) -> Result<&mut Self, TranscriptProofBuilderError> {
|
||||
let idx = Idx::new(ranges.to_range_set());
|
||||
let idx = ranges.to_range_set();
|
||||
|
||||
if idx.end() > self.transcript.len_of_direction(direction) {
|
||||
if idx.end().unwrap_or(0) > self.transcript.len_of_direction(direction) {
|
||||
return Err(TranscriptProofBuilderError::new(
|
||||
BuilderErrorKind::Index,
|
||||
format!(
|
||||
"range is out of bounds of the transcript ({}): {} > {}",
|
||||
direction,
|
||||
idx.end(),
|
||||
idx.end().unwrap_or(0),
|
||||
self.transcript.len_of_direction(direction)
|
||||
),
|
||||
));
|
||||
@@ -348,7 +357,10 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
let missing = idx.difference(committed);
|
||||
return Err(TranscriptProofBuilderError::new(
|
||||
BuilderErrorKind::MissingCommitment,
|
||||
format!("commitment is missing for ranges in {direction} transcript: {missing}"),
|
||||
format!(
|
||||
"commitment is missing for ranges in {direction} transcript: {}",
|
||||
FmtRangeSet(&missing)
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(self)
|
||||
@@ -403,25 +415,23 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (sent_dir_idxs, sent_uncovered) =
|
||||
uncovered_query_idx.sent.as_range_set().cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Sent),
|
||||
|(_, idx)| &idx.0,
|
||||
);
|
||||
let (sent_dir_idxs, sent_uncovered) = uncovered_query_idx.sent.cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Sent),
|
||||
|(_, idx)| idx,
|
||||
);
|
||||
// Uncovered ranges will be checked with ranges of the next
|
||||
// preferred commitment kind.
|
||||
uncovered_query_idx.sent = Idx(sent_uncovered);
|
||||
uncovered_query_idx.sent = sent_uncovered;
|
||||
|
||||
let (recv_dir_idxs, recv_uncovered) =
|
||||
uncovered_query_idx.recv.as_range_set().cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Received),
|
||||
|(_, idx)| &idx.0,
|
||||
);
|
||||
uncovered_query_idx.recv = Idx(recv_uncovered);
|
||||
let (recv_dir_idxs, recv_uncovered) = uncovered_query_idx.recv.cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Received),
|
||||
|(_, idx)| idx,
|
||||
);
|
||||
uncovered_query_idx.recv = recv_uncovered;
|
||||
|
||||
let dir_idxs = sent_dir_idxs
|
||||
.into_iter()
|
||||
@@ -439,25 +449,23 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
}
|
||||
}
|
||||
TranscriptCommitmentKind::Hash { alg } => {
|
||||
let (sent_hashes, sent_uncovered) =
|
||||
uncovered_query_idx.sent.as_range_set().cover_by(
|
||||
self.hash_secrets.iter().filter(|hash| {
|
||||
hash.direction == Direction::Sent && &hash.alg == alg
|
||||
}),
|
||||
|hash| &hash.idx.0,
|
||||
);
|
||||
let (sent_hashes, sent_uncovered) = uncovered_query_idx.sent.cover_by(
|
||||
self.hash_secrets.iter().filter(|hash| {
|
||||
hash.direction == Direction::Sent && &hash.alg == alg
|
||||
}),
|
||||
|hash| &hash.idx,
|
||||
);
|
||||
// Uncovered ranges will be checked with ranges of the next
|
||||
// preferred commitment kind.
|
||||
uncovered_query_idx.sent = Idx(sent_uncovered);
|
||||
uncovered_query_idx.sent = sent_uncovered;
|
||||
|
||||
let (recv_hashes, recv_uncovered) =
|
||||
uncovered_query_idx.recv.as_range_set().cover_by(
|
||||
self.hash_secrets.iter().filter(|hash| {
|
||||
hash.direction == Direction::Received && &hash.alg == alg
|
||||
}),
|
||||
|hash| &hash.idx.0,
|
||||
);
|
||||
uncovered_query_idx.recv = Idx(recv_uncovered);
|
||||
let (recv_hashes, recv_uncovered) = uncovered_query_idx.recv.cover_by(
|
||||
self.hash_secrets.iter().filter(|hash| {
|
||||
hash.direction == Direction::Received && &hash.alg == alg
|
||||
}),
|
||||
|hash| &hash.idx,
|
||||
);
|
||||
uncovered_query_idx.recv = recv_uncovered;
|
||||
|
||||
transcript_proof.hash_secrets.extend(
|
||||
sent_hashes
|
||||
@@ -577,7 +585,7 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_verify_missing_encoding_commitment_root() {
|
||||
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
|
||||
let idxs = vec![(Direction::Received, Idx::new(0..transcript.len().1))];
|
||||
let idxs = vec![(Direction::Received, RangeSet::from(0..transcript.len().1))];
|
||||
let encoding_tree = EncodingTree::new(
|
||||
&Blake3::default(),
|
||||
&idxs,
|
||||
@@ -632,15 +640,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_reveal_with_hash_commitment() {
|
||||
#[case::sha256(HashAlgId::SHA256)]
|
||||
#[case::blake3(HashAlgId::BLAKE3)]
|
||||
fn test_reveal_with_hash_commitment(#[case] alg: HashAlgId) {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
||||
let provider = HashProvider::default();
|
||||
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
|
||||
|
||||
let direction = Direction::Sent;
|
||||
let idx = Idx::new(0..10);
|
||||
let idx = RangeSet::from(0..10);
|
||||
let blinder: Blinder = rng.random();
|
||||
let alg = HashAlgId::SHA256;
|
||||
let hasher = provider.get(&alg).unwrap();
|
||||
|
||||
let commitment = PlaintextHash {
|
||||
@@ -678,15 +687,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_reveal_with_inconsistent_hash_commitment() {
|
||||
#[case::sha256(HashAlgId::SHA256)]
|
||||
#[case::blake3(HashAlgId::BLAKE3)]
|
||||
fn test_reveal_with_inconsistent_hash_commitment(#[case] alg: HashAlgId) {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
||||
let provider = HashProvider::default();
|
||||
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
|
||||
|
||||
let direction = Direction::Sent;
|
||||
let idx = Idx::new(0..10);
|
||||
let idx = RangeSet::from(0..10);
|
||||
let blinder: Blinder = rng.random();
|
||||
let alg = HashAlgId::SHA256;
|
||||
let hasher = provider.get(&alg).unwrap();
|
||||
|
||||
let commitment = PlaintextHash {
|
||||
@@ -894,10 +904,10 @@ mod tests {
|
||||
match kind {
|
||||
BuilderErrorKind::Cover { uncovered, .. } => {
|
||||
if !uncovered_sent_rangeset.is_empty() {
|
||||
assert_eq!(uncovered.sent, Idx(uncovered_sent_rangeset));
|
||||
assert_eq!(uncovered.sent, uncovered_sent_rangeset);
|
||||
}
|
||||
if !uncovered_recv_rangeset.is_empty() {
|
||||
assert_eq!(uncovered.recv, Idx(uncovered_recv_rangeset));
|
||||
assert_eq!(uncovered.recv, uncovered_recv_rangeset);
|
||||
}
|
||||
}
|
||||
_ => panic!("unexpected error kind: {kind:?}"),
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
use crate::{
|
||||
connection::{
|
||||
Certificate, HandshakeData, HandshakeDataV1_2, ServerEphemKey, ServerSignature, TlsVersion,
|
||||
VerifyData,
|
||||
CertBinding, CertBindingV1_2, ServerEphemKey, ServerSignature, TlsVersion, VerifyData,
|
||||
},
|
||||
transcript::{Direction, Transcript},
|
||||
webpki::CertificateDer,
|
||||
};
|
||||
use tls_core::msgs::{
|
||||
alert::AlertMessagePayload,
|
||||
@@ -19,9 +19,9 @@ use tls_core::msgs::{
|
||||
pub struct TlsTranscript {
|
||||
time: u64,
|
||||
version: TlsVersion,
|
||||
server_cert_chain: Option<Vec<Certificate>>,
|
||||
server_cert_chain: Option<Vec<CertificateDer>>,
|
||||
server_signature: Option<ServerSignature>,
|
||||
handshake_data: HandshakeData,
|
||||
certificate_binding: CertBinding,
|
||||
sent: Vec<Record>,
|
||||
recv: Vec<Record>,
|
||||
}
|
||||
@@ -32,9 +32,9 @@ impl TlsTranscript {
|
||||
pub fn new(
|
||||
time: u64,
|
||||
version: TlsVersion,
|
||||
server_cert_chain: Option<Vec<Certificate>>,
|
||||
server_cert_chain: Option<Vec<CertificateDer>>,
|
||||
server_signature: Option<ServerSignature>,
|
||||
handshake_data: HandshakeData,
|
||||
certificate_binding: CertBinding,
|
||||
verify_data: VerifyData,
|
||||
sent: Vec<Record>,
|
||||
recv: Vec<Record>,
|
||||
@@ -198,7 +198,7 @@ impl TlsTranscript {
|
||||
version,
|
||||
server_cert_chain,
|
||||
server_signature,
|
||||
handshake_data,
|
||||
certificate_binding,
|
||||
sent,
|
||||
recv,
|
||||
})
|
||||
@@ -215,7 +215,7 @@ impl TlsTranscript {
|
||||
}
|
||||
|
||||
/// Returns the server certificate chain.
|
||||
pub fn server_cert_chain(&self) -> Option<&[Certificate]> {
|
||||
pub fn server_cert_chain(&self) -> Option<&[CertificateDer]> {
|
||||
self.server_cert_chain.as_deref()
|
||||
}
|
||||
|
||||
@@ -226,17 +226,17 @@ impl TlsTranscript {
|
||||
|
||||
/// 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 {
|
||||
match &self.certificate_binding {
|
||||
CertBinding::V1_2(CertBindingV1_2 {
|
||||
server_ephemeral_key,
|
||||
..
|
||||
}) => server_ephemeral_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the handshake data.
|
||||
pub fn handshake_data(&self) -> &HandshakeData {
|
||||
&self.handshake_data
|
||||
/// Returns the certificate binding data.
|
||||
pub fn certificate_binding(&self) -> &CertBinding {
|
||||
&self.certificate_binding
|
||||
}
|
||||
|
||||
/// Returns the sent records.
|
||||
|
||||
168
crates/core/src/webpki.rs
Normal file
168
crates/core/src/webpki.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! Web PKI types.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use rustls_pki_types::{self as webpki_types, pem::PemObject};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::connection::ServerName;
|
||||
|
||||
/// X.509 certificate, DER encoded.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CertificateDer(pub Vec<u8>);
|
||||
|
||||
impl CertificateDer {
|
||||
/// Creates a DER-encoded certificate from a PEM-encoded certificate.
|
||||
pub fn from_pem_slice(pem: &[u8]) -> Result<Self, PemError> {
|
||||
let der = webpki_types::CertificateDer::from_pem_slice(pem).map_err(|_| PemError {})?;
|
||||
|
||||
Ok(Self(der.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Private key, DER encoded.
|
||||
#[derive(Debug, Clone, zeroize::ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub struct PrivateKeyDer(pub Vec<u8>);
|
||||
|
||||
impl PrivateKeyDer {
|
||||
/// Creates a DER-encoded private key from a PEM-encoded private key.
|
||||
pub fn from_pem_slice(pem: &[u8]) -> Result<Self, PemError> {
|
||||
let der = webpki_types::PrivateKeyDer::from_pem_slice(pem).map_err(|_| PemError {})?;
|
||||
|
||||
Ok(Self(der.secret_der().to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
/// PEM parsing error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("failed to parse PEM object")]
|
||||
pub struct PemError {}
|
||||
|
||||
/// Root certificate store.
|
||||
///
|
||||
/// This stores root certificates which are used to verify end-entity
|
||||
/// certificates presented by a TLS server.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RootCertStore {
|
||||
/// Unvalidated DER-encoded X.509 root certificates.
|
||||
pub roots: Vec<CertificateDer>,
|
||||
}
|
||||
|
||||
impl RootCertStore {
|
||||
/// Creates an empty root certificate store.
|
||||
pub fn empty() -> Self {
|
||||
Self { roots: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Server certificate verifier.
|
||||
#[derive(Debug)]
|
||||
pub struct ServerCertVerifier {
|
||||
roots: Vec<webpki_types::TrustAnchor<'static>>,
|
||||
}
|
||||
|
||||
impl ServerCertVerifier {
|
||||
/// Creates a new server certificate verifier.
|
||||
pub fn new(roots: &RootCertStore) -> Result<Self, ServerCertVerifierError> {
|
||||
let roots = roots
|
||||
.roots
|
||||
.iter()
|
||||
.map(|cert| {
|
||||
webpki::anchor_from_trusted_cert(&webpki_types::CertificateDer::from(
|
||||
cert.0.as_slice(),
|
||||
))
|
||||
.map(|anchor| anchor.to_owned())
|
||||
.map_err(|err| ServerCertVerifierError::InvalidRootCertificate {
|
||||
cert: cert.clone(),
|
||||
reason: err.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(Self { roots })
|
||||
}
|
||||
|
||||
/// Creates a new server certificate verifier with Mozilla root
|
||||
/// certificates.
|
||||
pub fn mozilla() -> Self {
|
||||
Self {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies the server certificate was valid at the given time of
|
||||
/// presentation.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `end_entity` - End-entity certificate to verify.
|
||||
/// * `intermediates` - Intermediate certificates to a trust anchor.
|
||||
/// * `server_name` - Server DNS name.
|
||||
/// * `time` - Unix time the certificate was presented.
|
||||
pub fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &CertificateDer,
|
||||
intermediates: &[CertificateDer],
|
||||
server_name: &ServerName,
|
||||
time: u64,
|
||||
) -> Result<(), ServerCertVerifierError> {
|
||||
let cert = webpki_types::CertificateDer::from(end_entity.0.as_slice());
|
||||
let cert = webpki::EndEntityCert::try_from(&cert).map_err(|e| {
|
||||
ServerCertVerifierError::InvalidEndEntityCertificate {
|
||||
cert: end_entity.clone(),
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
let intermediates = intermediates
|
||||
.iter()
|
||||
.map(|c| webpki_types::CertificateDer::from(c.0.as_slice()))
|
||||
.collect::<Vec<_>>();
|
||||
let server_name = server_name.to_webpki();
|
||||
let time = webpki_types::UnixTime::since_unix_epoch(Duration::from_secs(time));
|
||||
|
||||
cert.verify_for_usage(
|
||||
webpki::ALL_VERIFICATION_ALGS,
|
||||
&self.roots,
|
||||
&intermediates,
|
||||
time,
|
||||
webpki::KeyUsage::server_auth(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|_| ServerCertVerifierError::InvalidPath)?;
|
||||
|
||||
cert.verify_is_valid_for_subject_name(&server_name)
|
||||
.map_err(|_| ServerCertVerifierError::InvalidServerName)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Error for [`ServerCertVerifier`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("server certificate verification failed: {0}")]
|
||||
pub enum ServerCertVerifierError {
|
||||
/// Root certificate store contains invalid certificate.
|
||||
#[error("root certificate store contains invalid certificate: {reason}")]
|
||||
InvalidRootCertificate {
|
||||
/// Invalid certificate.
|
||||
cert: CertificateDer,
|
||||
/// Reason for invalidity.
|
||||
reason: String,
|
||||
},
|
||||
/// End-entity certificate is invalid.
|
||||
#[error("end-entity certificate is invalid: {reason}")]
|
||||
InvalidEndEntityCertificate {
|
||||
/// Invalid certificate.
|
||||
cert: CertificateDer,
|
||||
/// Reason for invalidity.
|
||||
reason: String,
|
||||
},
|
||||
/// Failed to verify certificate path to provided trust anchors.
|
||||
#[error("failed to verify certificate path to provided trust anchors")]
|
||||
InvalidPath,
|
||||
/// Failed to verify certificate is valid for provided server name.
|
||||
#[error("failed to verify certificate is valid for provided server name")]
|
||||
InvalidServerName,
|
||||
}
|
||||
@@ -8,11 +8,8 @@ version = "0.0.0"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
notary-client = { workspace = true }
|
||||
tlsn-core = { workspace = true }
|
||||
tlsn = { workspace = true }
|
||||
tlsn-formats = { workspace = true }
|
||||
tlsn-tls-core = { workspace = true }
|
||||
tls-server-fixture = { workspace = true }
|
||||
tlsn-server-fixture = { workspace = true }
|
||||
tlsn-server-fixture-certs = { workspace = true }
|
||||
@@ -21,13 +18,13 @@ spansy = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
dotenv = { version = "0.15.0" }
|
||||
futures = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
hyper = { workspace = true, features = ["client", "http1"] }
|
||||
hyper-util = { workspace = true, features = ["full"] }
|
||||
k256 = { workspace = true, features = ["ecdsa"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"rt",
|
||||
@@ -40,6 +37,16 @@ tokio = { workspace = true, features = [
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
noir = { git = "https://github.com/zkmopro/noir-rs", tag = "v1.0.0-beta.8", features = ["barretenberg"] }
|
||||
blake3 = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "interactive"
|
||||
path = "interactive/interactive.rs"
|
||||
|
||||
[[example]]
|
||||
name = "interactive_zk"
|
||||
path = "interactive_zk/interactive_zk.rs"
|
||||
|
||||
[[example]]
|
||||
name = "attestation_prove"
|
||||
@@ -52,7 +59,3 @@ path = "attestation/present.rs"
|
||||
[[example]]
|
||||
name = "attestation_verify"
|
||||
path = "attestation/verify.rs"
|
||||
|
||||
[[example]]
|
||||
name = "interactive"
|
||||
path = "interactive/interactive.rs"
|
||||
|
||||
@@ -5,4 +5,4 @@ This folder contains examples demonstrating how to use the TLSNotary protocol.
|
||||
* [Interactive](./interactive/README.md): Interactive Prover and Verifier session without a trusted notary.
|
||||
* [Attestation](./attestation/README.md): Performing a simple notarization with a trusted notary.
|
||||
|
||||
Refer to <https://docs.tlsnotary.org/quick_start/index.html> for a quick start guide to using TLSNotary with these examples.
|
||||
Refer to <https://tlsnotary.org/docs/quick_start> for a quick start guide to using TLSNotary with these examples.
|
||||
@@ -1,75 +1,81 @@
|
||||
## Simple Attestation Example: Notarize Public Data from example.com (Rust) <a name="rust-simple"></a>
|
||||
# Attestation Example
|
||||
|
||||
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.
|
||||
This example demonstrates a **TLSNotary attestation workflow**: notarizing data from a server with a trusted third party (Notary), then creating verifiable presentations with selective disclosure of sensitive information to a Verifier.
|
||||
|
||||
### 1. Notarize
|
||||
## 🔍 How It Works
|
||||
|
||||
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):
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant P as Prover
|
||||
participant N as MPC-TLS<br/>Verifier
|
||||
participant S as Server<br/>Fixture
|
||||
participant V as Attestation<br/>Verifier
|
||||
|
||||
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
|
||||
```
|
||||
Note over P,S: 1. Notarization Phase
|
||||
P->>N: Establish MPC-TLS connection
|
||||
P->>S: Request (MPC-TLS)
|
||||
S->>P: Response (MPC-TLS)
|
||||
N->>P: Issue signed attestation
|
||||
|
||||
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
|
||||
Note over P: 2. Presentation Phase
|
||||
P->>P: Create redacted presentation
|
||||
|
||||
Note over P,V: 3. Verification Phase
|
||||
P->>V: Share presentation
|
||||
V->>V: Verify attestation signature
|
||||
```
|
||||
|
||||
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!
|
||||
### The Three-Step Process
|
||||
|
||||
1. **🔐 Notarize**: Prover collaborates with Notary to create an authenticated TLS session and obtain a signed attestation
|
||||
2. **✂️ Present**: Prover creates a selective presentation, choosing which data to reveal or redact
|
||||
3. **✅ Verify**: Anyone can verify the presentation's authenticity using the Notary's public key
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Step 1: Notarize Data
|
||||
|
||||
**Start the test server** (from repository root):
|
||||
```bash
|
||||
RUST_LOG=info PORT=4000 cargo run --bin tlsn-server-fixture
|
||||
```
|
||||
|
||||
**Run the notarization** (in a new terminal):
|
||||
```bash
|
||||
RUST_LOG=info SERVER_PORT=4000 cargo run --release --example attestation_prove
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
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.
|
||||
### Step 2: Create Verifiable Presentation
|
||||
|
||||
### 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
|
||||
**Generate a redacted presentation:**
|
||||
```bash
|
||||
cargo run --release --example attestation_present
|
||||
```
|
||||
|
||||
If successful, you’ll see this output in the console:
|
||||
|
||||
```log
|
||||
**Expected output:**
|
||||
```
|
||||
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!
|
||||
> 💡 **Tip**: You can create multiple presentations from the same attestation, each with different redactions!
|
||||
|
||||
### 3. Verify the Presentation
|
||||
### Step 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
|
||||
**Verify the presentation:**
|
||||
```bash
|
||||
cargo run --release --example attestation_verify
|
||||
```
|
||||
|
||||
Upon success, you should see output similar to:
|
||||
```log
|
||||
**Expected output:**
|
||||
```
|
||||
Verifying presentation with {key algorithm} key: { hex encoded key }
|
||||
|
||||
**Ask yourself, do you trust this key?**
|
||||
@@ -79,33 +85,80 @@ Successfully verified that the data below came from a session with test-server.i
|
||||
Note that the data which the Prover chose not to disclose are shown as X.
|
||||
|
||||
Data sent:
|
||||
...
|
||||
|
||||
GET /formats/json HTTP/1.1
|
||||
host: test-server.io
|
||||
accept: */*
|
||||
accept-encoding: identity
|
||||
connection: close
|
||||
user-agent: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
Data received:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
content-type: application/json
|
||||
content-length: 722
|
||||
connection: close
|
||||
date: Mon, 08 Sep 2025 09:18:29 GMT
|
||||
|
||||
XXXXXX1234567890XXXXXXXXXXXXXXXXXXXXXXXXJohn DoeXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1.2XX
|
||||
```
|
||||
|
||||
⚠️ 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.
|
||||
## 🎯 Use Cases & Examples
|
||||
|
||||
### HTML
|
||||
### JSON Data (Default)
|
||||
Perfect for API responses, configuration data, or structured information:
|
||||
```bash
|
||||
# All three steps use JSON by default
|
||||
SERVER_PORT=4000 cargo run --release --example attestation_prove
|
||||
cargo run --release --example attestation_present
|
||||
cargo run --release --example attestation_verify
|
||||
```
|
||||
|
||||
In the example above, we notarized a JSON response. TLSNotary also supports notarizing HTML content. To run an HTML example, use:
|
||||
|
||||
```shell
|
||||
# notarize
|
||||
### HTML Content
|
||||
Ideal for web pages, forms, or any HTML-based data:
|
||||
```bash
|
||||
# Notarize HTML content
|
||||
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
|
||||
### Authenticated/Private Data
|
||||
For APIs requiring authentication tokens, cookies, or private access:
|
||||
```bash
|
||||
# Notarize private data with authentication
|
||||
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
|
||||
```
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
For detailed logging and troubleshooting:
|
||||
```bash
|
||||
RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example attestation_prove
|
||||
```
|
||||
|
||||
### Generated Files
|
||||
|
||||
After running the examples, you'll find:
|
||||
- **`*.attestation.tlsn`**: The cryptographically signed attestation from the Notary
|
||||
- **`*.secrets.tlsn`**: Cryptographic secrets needed to create presentations
|
||||
- **`*.presentation.tlsn`**: The verifiable presentation with your chosen redactions
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### Trust Model
|
||||
- ✅ **Notary Key**: The presentation includes the Notary's verifying key - The verifier must trust this key
|
||||
- ✅ **Data Authenticity**: Cryptographically guaranteed that data came from the specified server
|
||||
- ✅ **Tamper Evidence**: Any modification to the presentation will fail verification
|
||||
- ⚠️ **Notary Trust**: The verifier must trust the Notary not to collude with the Prover
|
||||
|
||||
### Production Deployment
|
||||
- 🏭 **Independent Notary**: Use a trusted third-party Notary service (not a local one)
|
||||
- 🔒 **Key Management**: Implement proper Notary key distribution and verification
|
||||
- 📋 **Audit Trail**: Maintain logs of notarization and verification events
|
||||
- 🔄 **Key Rotation**: Plan for Notary key updates and migration
|
||||
|
||||
> ⚠️ **Demo Notice**: This example uses a local test server and local Notary for demonstration. In production, use trusted third-party Notary services and real server endpoints.
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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.
|
||||
// an HTTP request sent to a server fixture. The attestation and secrets are
|
||||
// saved to disk.
|
||||
|
||||
use std::env;
|
||||
|
||||
@@ -9,21 +9,31 @@ use http_body_util::Empty;
|
||||
use hyper::{body::Bytes, Request, StatusCode};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use spansy::Spanned;
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
sync::oneshot::{self, Receiver, Sender},
|
||||
};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::debug;
|
||||
use tracing::info;
|
||||
|
||||
use notary_client::{Accepted, NotarizationRequest, NotaryClient};
|
||||
use tls_server_fixture::{CA_CERT_DER, SERVER_DOMAIN};
|
||||
use tlsn::{
|
||||
attestation::request::RequestConfig,
|
||||
config::ProtocolConfig,
|
||||
prover::{Prover, ProverConfig, TlsConfig},
|
||||
transcript::TranscriptCommitConfig,
|
||||
attestation::{
|
||||
request::{Request as AttestationRequest, RequestConfig},
|
||||
signing::Secp256k1Signer,
|
||||
Attestation, AttestationConfig, CryptoProvider, Secrets,
|
||||
},
|
||||
config::{
|
||||
CertificateDer, PrivateKeyDer, ProtocolConfig, ProtocolConfigValidator, RootCertStore,
|
||||
},
|
||||
connection::{ConnectionInfo, HandshakeData, ServerName, TranscriptLength},
|
||||
prover::{state::Committed, ProveConfig, Prover, ProverConfig, ProverOutput, TlsConfig},
|
||||
transcript::{ContentType, TranscriptCommitConfig},
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
use tlsn_examples::ExampleType;
|
||||
use tlsn_formats::http::{DefaultHttpCommitter, HttpCommit, HttpTranscript};
|
||||
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
|
||||
use tlsn_server_fixture_certs::{CLIENT_CERT, CLIENT_KEY};
|
||||
use tlsn_server_fixture_certs::{CA_CERT_DER, CLIENT_CERT_DER, CLIENT_KEY_DER, SERVER_DOMAIN};
|
||||
|
||||
// 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";
|
||||
@@ -38,75 +48,72 @@ struct Args {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
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
|
||||
let (notary_socket, prover_socket) = tokio::io::duplex(1 << 23);
|
||||
let (request_tx, request_rx) = oneshot::channel();
|
||||
let (attestation_tx, attestation_rx) = oneshot::channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
notary(notary_socket, request_rx, attestation_tx)
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
prover(
|
||||
prover_socket,
|
||||
request_tx,
|
||||
attestation_rx,
|
||||
uri,
|
||||
extra_headers,
|
||||
&args.example_type,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notarize(
|
||||
async fn prover<S: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
socket: S,
|
||||
req_tx: Sender<AttestationRequest>,
|
||||
resp_rx: Receiver<Attestation>,
|
||||
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();
|
||||
// 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 tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
// (Optional) Set up TLS client authentication if required by the server.
|
||||
.client_auth((
|
||||
vec![CertificateDer(CLIENT_CERT_DER.to_vec())],
|
||||
PrivateKeyDer(CLIENT_KEY_DER.to_vec()),
|
||||
));
|
||||
|
||||
// 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 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)
|
||||
.server_name(ServerName::Dns(SERVER_DOMAIN.try_into().unwrap()))
|
||||
.tls_config(tls_config)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
// We must configure the amount of data we expect to exchange beforehand, which will
|
||||
@@ -117,20 +124,10 @@ async fn notarize(
|
||||
.build()?,
|
||||
);
|
||||
|
||||
// (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?;
|
||||
let prover = Prover::new(prover_config).setup(socket.compat()).await?;
|
||||
|
||||
// Open a TCP connection to the server.
|
||||
let client_socket = tokio::net::TcpStream::connect((server_host, server_port)).await?;
|
||||
@@ -168,12 +165,12 @@ async fn notarize(
|
||||
}
|
||||
let request = request_builder.body(Empty::<Bytes>::new())?;
|
||||
|
||||
println!("Starting an MPC TLS connection with the server");
|
||||
info!("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());
|
||||
info!("Got a response from the server: {}", response.status());
|
||||
|
||||
assert!(response.status() == StatusCode::OK);
|
||||
|
||||
@@ -189,10 +186,10 @@ async fn notarize(
|
||||
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)?);
|
||||
info!("{}", serde_json::to_string_pretty(&parsed)?);
|
||||
}
|
||||
tlsn_formats::http::BodyContent::Unknown(_span) => {
|
||||
debug!("{}", &body);
|
||||
info!("{}", &body);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -220,10 +217,7 @@ async fn notarize(
|
||||
|
||||
let request_config = builder.build()?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
let (attestation, secrets) = prover.notarize(&request_config).await?;
|
||||
|
||||
println!("Notarization complete!");
|
||||
let (attestation, secrets) = notarize(&mut prover, &request_config, req_tx, resp_rx).await?;
|
||||
|
||||
// Write the attestation to disk.
|
||||
let attestation_path = tlsn_examples::get_file_path(example_type, "attestation");
|
||||
@@ -242,3 +236,161 @@ async fn notarize(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notarize(
|
||||
prover: &mut Prover<Committed>,
|
||||
config: &RequestConfig,
|
||||
request_tx: Sender<AttestationRequest>,
|
||||
attestation_rx: Receiver<Attestation>,
|
||||
) -> Result<(Attestation, Secrets), Box<dyn std::error::Error>> {
|
||||
let mut builder = ProveConfig::builder(prover.transcript());
|
||||
|
||||
if let Some(config) = config.transcript_commit() {
|
||||
builder.transcript_commit(config.clone());
|
||||
}
|
||||
|
||||
let disclosure_config = builder.build()?;
|
||||
|
||||
let ProverOutput {
|
||||
transcript_commitments,
|
||||
transcript_secrets,
|
||||
..
|
||||
} = prover.prove(&disclosure_config).await?;
|
||||
|
||||
// Build an attestation request.
|
||||
let mut builder = AttestationRequest::builder(config);
|
||||
|
||||
builder
|
||||
.server_name(ServerName::Dns(SERVER_DOMAIN.try_into().unwrap()))
|
||||
.handshake_data(HandshakeData {
|
||||
certs: prover
|
||||
.tls_transcript()
|
||||
.server_cert_chain()
|
||||
.expect("server cert chain is present")
|
||||
.to_vec(),
|
||||
sig: prover
|
||||
.tls_transcript()
|
||||
.server_signature()
|
||||
.expect("server signature is present")
|
||||
.clone(),
|
||||
binding: prover.tls_transcript().certificate_binding().clone(),
|
||||
})
|
||||
.transcript(prover.transcript().clone())
|
||||
.transcript_commitments(transcript_secrets, transcript_commitments);
|
||||
|
||||
let (request, secrets) = builder.build(&CryptoProvider::default())?;
|
||||
|
||||
// Send attestation request to notary.
|
||||
request_tx
|
||||
.send(request.clone())
|
||||
.map_err(|_| "notary is not receiving attestation request".to_string())?;
|
||||
|
||||
// Receive attestation from notary.
|
||||
let attestation = attestation_rx
|
||||
.await
|
||||
.map_err(|err| format!("notary did not respond with attestation: {err}"))?;
|
||||
|
||||
// Check the attestation is consistent with the Prover's view.
|
||||
request.validate(&attestation)?;
|
||||
|
||||
Ok((attestation, secrets))
|
||||
}
|
||||
|
||||
async fn notary<S: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
socket: S,
|
||||
request_rx: Receiver<AttestationRequest>,
|
||||
attestation_tx: Sender<Attestation>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set up Verifier.
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(tlsn_examples::MAX_SENT_DATA)
|
||||
.max_recv_data(tlsn_examples::MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// 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 verifier_config = VerifierConfig::builder()
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut verifier = Verifier::new(verifier_config)
|
||||
.setup(socket.compat())
|
||||
.await?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
let VerifierOutput {
|
||||
transcript_commitments,
|
||||
..
|
||||
} = verifier.verify(&VerifyConfig::default()).await?;
|
||||
|
||||
let tls_transcript = verifier.tls_transcript().clone();
|
||||
|
||||
verifier.close().await?;
|
||||
|
||||
let sent_len = tls_transcript
|
||||
.sent()
|
||||
.iter()
|
||||
.filter_map(|record| {
|
||||
if let ContentType::ApplicationData = record.typ {
|
||||
Some(record.ciphertext.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum::<usize>();
|
||||
|
||||
let recv_len = tls_transcript
|
||||
.recv()
|
||||
.iter()
|
||||
.filter_map(|record| {
|
||||
if let ContentType::ApplicationData = record.typ {
|
||||
Some(record.ciphertext.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum::<usize>();
|
||||
|
||||
// Receive attestation request from prover.
|
||||
let request = request_rx.await?;
|
||||
|
||||
// Load a dummy signing key.
|
||||
let signing_key = k256::ecdsa::SigningKey::from_bytes(&[1u8; 32].into())?;
|
||||
let signer = Box::new(Secp256k1Signer::new(&signing_key.to_bytes())?);
|
||||
let mut provider = CryptoProvider::default();
|
||||
provider.signer.set_signer(signer);
|
||||
|
||||
// Build an attestation.
|
||||
let mut att_config_builder = AttestationConfig::builder();
|
||||
att_config_builder.supported_signature_algs(Vec::from_iter(provider.signer.supported_algs()));
|
||||
let att_config = att_config_builder.build()?;
|
||||
|
||||
let mut builder = Attestation::builder(&att_config).accept_request(request)?;
|
||||
builder
|
||||
.connection_info(ConnectionInfo {
|
||||
time: tls_transcript.time(),
|
||||
version: (*tls_transcript.version()),
|
||||
transcript_length: TranscriptLength {
|
||||
sent: sent_len as u32,
|
||||
received: recv_len as u32,
|
||||
},
|
||||
})
|
||||
.server_ephemeral_key(tls_transcript.server_ephemeral_key().clone())
|
||||
.transcript_commitments(transcript_commitments);
|
||||
|
||||
let attestation = builder.build(&provider)?;
|
||||
|
||||
// Send attestation to prover.
|
||||
attestation_tx
|
||||
.send(attestation)
|
||||
.map_err(|_| "prover is not receiving attestation".to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,14 +6,17 @@ use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use tls_core::verify::WebPkiVerifier;
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::attestation::{
|
||||
presentation::{Presentation, PresentationOutput},
|
||||
signing::VerifyingKey,
|
||||
CryptoProvider,
|
||||
use tlsn::{
|
||||
attestation::{
|
||||
presentation::{Presentation, PresentationOutput},
|
||||
signing::VerifyingKey,
|
||||
CryptoProvider,
|
||||
},
|
||||
config::{CertificateDer, RootCertStore},
|
||||
verifier::ServerCertVerifier,
|
||||
};
|
||||
use tlsn_examples::ExampleType;
|
||||
use tlsn_server_fixture_certs::CA_CERT_DER;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
@@ -41,12 +44,11 @@ async fn verify_presentation(example_type: &ExampleType) -> Result<(), Box<dyn s
|
||||
//
|
||||
// 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 root_cert_store = RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
};
|
||||
let crypto_provider = CryptoProvider {
|
||||
cert: WebPkiVerifier::new(root_store, None),
|
||||
cert: ServerCertVerifier::new(&root_cert_store)?,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::instrument;
|
||||
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::{
|
||||
config::{ProtocolConfig, ProtocolConfigValidator},
|
||||
config::{CertificateDer, ProtocolConfig, ProtocolConfigValidator, RootCertStore},
|
||||
connection::ServerName,
|
||||
prover::{ProveConfig, Prover, ProverConfig, TlsConfig},
|
||||
transcript::PartialTranscript,
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
|
||||
use tlsn_server_fixture_certs::SERVER_DOMAIN;
|
||||
use tlsn_server_fixture_certs::{CA_CERT_DER, SERVER_DOMAIN};
|
||||
|
||||
const SECRET: &str = "TLSNotary's private key 🤡";
|
||||
|
||||
@@ -72,18 +72,16 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
// 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 mut tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder.root_store(root_store);
|
||||
tls_config_builder.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
});
|
||||
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)
|
||||
.server_name(ServerName::Dns(server_domain.try_into().unwrap()))
|
||||
.tls_config(tls_config)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
@@ -194,13 +192,10 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
// 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 verifier_config = VerifierConfig::builder()
|
||||
.root_store(root_store)
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()
|
||||
.unwrap();
|
||||
@@ -234,6 +229,7 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
.unwrap_or_else(|| panic!("Expected valid data from {SERVER_DOMAIN}"));
|
||||
|
||||
// Check Session info: server name.
|
||||
let ServerName::Dns(server_name) = server_name;
|
||||
assert_eq!(server_name.as_str(), SERVER_DOMAIN);
|
||||
|
||||
transcript
|
||||
|
||||
5
crates/examples/interactive_zk/.gitignore
vendored
Normal file
5
crates/examples/interactive_zk/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
!noir/target/
|
||||
# Ignore everything inside noir/target
|
||||
noir/target/*
|
||||
# Except noir.json
|
||||
!noir/target/noir.json
|
||||
167
crates/examples/interactive_zk/README.md
Normal file
167
crates/examples/interactive_zk/README.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Interactive Zero-Knowledge Age Verification with TLSNotary
|
||||
|
||||
This example demonstrates **privacy-preserving age verification** using TLSNotary and zero-knowledge proofs. It allows a prover to demonstrate they are 18+ years old without revealing their actual birth date or any other personal information.
|
||||
|
||||
## 🔍 How It Works (simplified overview)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Tax Server<br/>(fixture)
|
||||
participant P as Prover
|
||||
participant V as Verifier
|
||||
|
||||
P->>S: Request tax data (with auth token) (MPC-TLS)
|
||||
S->>P: Tax data including `date_of_birth` (MPC-TLS)
|
||||
P->>V: Share transcript with redactions
|
||||
P->>V: Commit to blinded hash of birth date
|
||||
P->>P: Generate ZK proof of age ≥ 18
|
||||
P->>V: Send ZK proof
|
||||
V->>V: Verify transcript & ZK proof
|
||||
V->>V: ✅ Confirm: Prover is 18+ (no birth date revealed)
|
||||
```
|
||||
|
||||
### The Process
|
||||
|
||||
1. **MPC-TLS Session**: The Prover fetches tax information containing their birth date, while the Verifier jointly verifies the TLS session to ensure the data comes from the authentic server.
|
||||
2. **Selective Disclosure**:
|
||||
* The authorization token is **redacted**: the Verifier sees the plaintext request but not the token.
|
||||
* The birth date is **committed** as a blinded hash: the Verifier cannot see the date, but the Prover is cryptographically bound to it.
|
||||
(Depending on the use case more data can be redacted or revealed)
|
||||
3. **Zero-Knowledge Proof**: The Prover generates a ZK proof that the committed birth date corresponds to an age ≥ 18.
|
||||
4. **Verification**: The Verifier checks both the TLS transcript and the ZK proof, confirming age ≥ 18 without learning the actual date of birth.
|
||||
|
||||
|
||||
### Example Data
|
||||
|
||||
The tax server returns data like this:
|
||||
```json
|
||||
{
|
||||
"tax_year": 2024,
|
||||
"taxpayer": {
|
||||
"idnr": "12345678901",
|
||||
"first_name": "Max",
|
||||
"last_name": "Mustermann",
|
||||
"date_of_birth": "1985-03-12",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 Zero-Knowledge Proof Details
|
||||
|
||||
The ZK circuit proves: **"I know a birth date that hashes to the committed value AND indicates I am 18+ years old"**
|
||||
|
||||
**Public Inputs:**
|
||||
- ✅ Verification date
|
||||
- ✅ Committed blinded hash of birth date
|
||||
|
||||
**Private Inputs (Hidden):**
|
||||
- 🔒 Actual birth date plaintext
|
||||
- 🔒 Random blinder used in hash commitment
|
||||
|
||||
**What the Verifier Learns:**
|
||||
- ✅ The prover is 18+ years old
|
||||
- ✅ The birth date is authentic (from the MPC-TLS session)
|
||||
|
||||
Everything else remains private.
|
||||
|
||||
## 🏃 Run the Example
|
||||
|
||||
1. **Start the test server** (from repository root):
|
||||
```bash
|
||||
RUST_LOG=info PORT=4000 cargo run --bin tlsn-server-fixture
|
||||
```
|
||||
|
||||
2. **Run the age verification** (in a new terminal):
|
||||
```bash
|
||||
SERVER_PORT=4000 cargo run --release --example interactive_zk
|
||||
```
|
||||
|
||||
3. **For detailed logs**:
|
||||
```bash
|
||||
RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example interactive_zk
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
Successfully verified https://test-server.io:4000/elster
|
||||
Age verified in ZK: 18+ ✅
|
||||
|
||||
Verified sent data:
|
||||
GET https://test-server.io:4000/elster HTTP/1.1
|
||||
host: test-server.io
|
||||
connection: close
|
||||
authorization: 🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈
|
||||
|
||||
Verified received data:
|
||||
🙈🙈🙈🙈🙈🙈🙈🙈[truncated for brevity]...🙈🙈🙈🙈🙈"tax_year":2024🙈🙈🙈🙈🙈...
|
||||
```
|
||||
|
||||
> 💡 **Note**: In this demo, both Prover and Verifier run on the same machine. In production, they would operate on separate systems.
|
||||
> 💡 **Note**: This demo assumes that the tax server serves correct data, and that only the submitter of the tax data has access to the specified page.
|
||||
|
||||
## 🛠 Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
interactive_zk/
|
||||
├── prover.rs # Prover implementation
|
||||
├── verifier.rs # Verifier implementation
|
||||
├── types.rs # Shared types
|
||||
└── interactive_zk.rs # Main example runner
|
||||
├── noir/ # Zero-knowledge circuit
|
||||
│ ├── src/main.n # Noir circuit code
|
||||
│ ├── target/ # Compiled circuit artifacts
|
||||
│ └── Nargo.toml # Noir project config
|
||||
│ └── Prover.toml # Example input for `nargo execute`
|
||||
│ └── generate_test_data.rs # Rust script to generate Noir test data
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Noir Circuit Commands
|
||||
|
||||
We use [Mopro's `noir_rs`](https://zkmopro.org/docs/crates/noir-rs/) for ZK proof generation. The **circuit is pre-compiled and ready to use**. You don't need to install Noir tools to run the example. But if you want to change or test the circuit in isolation, you can use the following instructions.
|
||||
|
||||
Before you proceed, we recommend to double check that your Noir tooling matches the versions used in Mopro's `noir_rs`:
|
||||
```sh
|
||||
# Install correct Noir and BB versions (important for compatibility!)
|
||||
noirup --version 1.0.0-beta.8
|
||||
bbup -v 1.0.0-nightly.20250723
|
||||
```
|
||||
|
||||
If you don't have `noirup` and `bbup` installed yet, check [Noir's Quick Start](https://noir-lang.org/docs/getting_started/quick_start).
|
||||
|
||||
To compile the circuit, go to the `noir` folder and run `nargo compile`.
|
||||
|
||||
To check and experiment with the Noir circuit, you can use these commands:
|
||||
|
||||
* Execute Circuit: Compile the circuit and run it with sample data from `Prover.toml`:
|
||||
```sh
|
||||
nargo execute
|
||||
```
|
||||
* Generate Verification Key: Create the verification key needed to verify proofs
|
||||
```sh
|
||||
bb write_vk -b ./target/noir.json -o ./target
|
||||
```
|
||||
* Generate Proof: Create a zero-knowledge proof using the circuit and witness data.
|
||||
```sh
|
||||
bb prove --bytecode_path ./target/noir.json --witness_path ./target/noir.gz -o ./target
|
||||
```
|
||||
* Verify Proof: Verify that a proof is valid using the verification key.
|
||||
```sh
|
||||
bb verify -k ./target/vk -p ./target/proof
|
||||
```
|
||||
* Run the Noir tests:
|
||||
```sh
|
||||
nargo test --show-output
|
||||
```
|
||||
To create extra tests, you can use `./generate_test_data.rs` to help with generating correct blinders and hashes.
|
||||
|
||||
## 📚 Learn More
|
||||
|
||||
- [TLSNotary Documentation](https://docs.tlsnotary.org/)
|
||||
- [Noir Language Guide](https://noir-lang.org/)
|
||||
- [Zero-Knowledge Proofs Explained](https://ethereum.org/en/zero-knowledge-proofs/)
|
||||
- [Mopro ZK Toolkit](https://zkmopro.org/)
|
||||
59
crates/examples/interactive_zk/interactive_zk.rs
Normal file
59
crates/examples/interactive_zk/interactive_zk.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
mod prover;
|
||||
mod types;
|
||||
mod verifier;
|
||||
|
||||
use prover::prover;
|
||||
use std::{
|
||||
env,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
|
||||
use tlsn_server_fixture_certs::SERVER_DOMAIN;
|
||||
use verifier::verifier;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let server_host: String = env::var("SERVER_HOST").unwrap_or("127.0.0.1".into());
|
||||
let server_port: u16 = env::var("SERVER_PORT")
|
||||
.map(|port| port.parse().expect("port should be valid integer"))
|
||||
.unwrap_or(DEFAULT_FIXTURE_PORT);
|
||||
|
||||
// We use SERVER_DOMAIN here to make sure it matches the domain in the test
|
||||
// server's certificate.
|
||||
let uri = format!("https://{SERVER_DOMAIN}:{server_port}/elster");
|
||||
let server_ip: IpAddr = server_host
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid IP address '{}': {}", server_host, e))?;
|
||||
let server_addr = SocketAddr::from((server_ip, server_port));
|
||||
|
||||
// Connect prover and verifier.
|
||||
let (prover_socket, verifier_socket) = tokio::io::duplex(1 << 23);
|
||||
let (prover_extra_socket, verifier_extra_socket) = tokio::io::duplex(1 << 23);
|
||||
|
||||
let (_, transcript) = tokio::try_join!(
|
||||
prover(prover_socket, prover_extra_socket, &server_addr, &uri),
|
||||
verifier(verifier_socket, verifier_extra_socket)
|
||||
)?;
|
||||
|
||||
println!("---");
|
||||
println!("Successfully verified {}", &uri);
|
||||
println!("Age verified in ZK: 18+ ✅\n");
|
||||
|
||||
println!(
|
||||
"Verified sent data:\n{}",
|
||||
bytes_to_redacted_string(transcript.sent_unsafe())
|
||||
);
|
||||
println!(
|
||||
"Verified received data:\n{}",
|
||||
bytes_to_redacted_string(transcript.received_unsafe())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render redacted bytes as `🙈`.
|
||||
pub fn bytes_to_redacted_string(bytes: &[u8]) -> String {
|
||||
String::from_utf8_lossy(bytes).replace('\0', "🙈")
|
||||
}
|
||||
7
crates/examples/interactive_zk/noir/Nargo.toml
Normal file
7
crates/examples/interactive_zk/noir/Nargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "noir"
|
||||
type = "bin"
|
||||
authors = [""]
|
||||
|
||||
[dependencies]
|
||||
date = { tag = "v0.5.4", git = "https://github.com/madztheo/noir-date.git" }
|
||||
8
crates/examples/interactive_zk/noir/Prover.toml
Normal file
8
crates/examples/interactive_zk/noir/Prover.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
blinder = [108, 93, 120, 205, 15, 35, 159, 124, 243, 96, 22, 128, 16, 149, 219, 216]
|
||||
committed_hash = [186, 158, 101, 39, 49, 48, 26, 83, 242, 96, 10, 221, 121, 174, 62, 50, 136, 132, 232, 58, 25, 32, 66, 196, 99, 85, 66, 85, 255, 1, 202, 254]
|
||||
date_of_birth = "1985-03-12"
|
||||
|
||||
[proof_date]
|
||||
day = "29"
|
||||
month = "08"
|
||||
year = "2025"
|
||||
64
crates/examples/interactive_zk/noir/generate_test_data.rs
Executable file
64
crates/examples/interactive_zk/noir/generate_test_data.rs
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env -S cargo +nightly -Zscript
|
||||
---
|
||||
[package]
|
||||
name = "generate_test_data"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
blake3 = "1.5"
|
||||
rand = "0.8"
|
||||
chrono = "0.4"
|
||||
---
|
||||
use chrono::Datelike;
|
||||
use chrono::Local;
|
||||
use rand::RngCore;
|
||||
|
||||
fn main() {
|
||||
// 1. Birthdate string (fixed)
|
||||
let dob_str = "1985-03-12"; // 10 bytes long
|
||||
|
||||
let proof_date = Local::now().date_naive();
|
||||
let proof_year = proof_date.year();
|
||||
let proof_month = proof_date.month();
|
||||
let proof_day = proof_date.day();
|
||||
|
||||
// 2. Generate random 16-byte blinder
|
||||
let mut blinder = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut blinder);
|
||||
|
||||
// 3. Concatenate blinder + dob string bytes
|
||||
let mut preimage = Vec::with_capacity(26);
|
||||
preimage.extend_from_slice(dob_str.as_bytes());
|
||||
preimage.extend_from_slice(&blinder);
|
||||
|
||||
// 4. Hash it
|
||||
let hash = blake3::hash(&preimage);
|
||||
let hash = hash.as_bytes();
|
||||
|
||||
let blinder = blinder
|
||||
.iter()
|
||||
.map(|b| b.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let committed_hash = hash
|
||||
.iter()
|
||||
.map(|b| b.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
println!(
|
||||
"
|
||||
// Private input
|
||||
let date_of_birth = \"{dob_str}\";
|
||||
let blinder = [{blinder}];
|
||||
|
||||
// Public input
|
||||
let proof_date = date::Date {{ year: {proof_year}, month: {proof_month}, day: {proof_day} }};
|
||||
let committed_hash = [{committed_hash}];
|
||||
|
||||
main(proof_date, committed_hash, date_of_birth, blinder);
|
||||
"
|
||||
);
|
||||
}
|
||||
82
crates/examples/interactive_zk/noir/src/main.nr
Normal file
82
crates/examples/interactive_zk/noir/src/main.nr
Normal file
@@ -0,0 +1,82 @@
|
||||
use dep::date::Date;
|
||||
|
||||
fn main(
|
||||
// Public inputs
|
||||
proof_date: pub date::Date, // "2025-08-29"
|
||||
committed_hash: pub [u8; 32], // Hash of (blinder || dob string)
|
||||
// Private inputs
|
||||
date_of_birth: str<10>, // "1985-03-12"
|
||||
blinder: [u8; 16], // Random 16-byte blinder
|
||||
) {
|
||||
let is_18 = check_18(date_of_birth, proof_date);
|
||||
|
||||
let correct_hash = check_hash(date_of_birth, blinder, committed_hash);
|
||||
|
||||
assert(correct_hash);
|
||||
assert(is_18);
|
||||
}
|
||||
|
||||
fn check_18(date_of_birth: str<10>, proof_date: date::Date) -> bool {
|
||||
let dob = parse_birth_date(date_of_birth);
|
||||
let is_18 = dob.add_years(18).lt(proof_date);
|
||||
println(f"Is 18? {is_18}");
|
||||
is_18
|
||||
}
|
||||
|
||||
fn check_hash(date_of_birth: str<10>, blinder: [u8; 16], committed_hash: [u8; 32]) -> bool {
|
||||
let hash_input: [u8; 26] = make_hash_input(date_of_birth, blinder);
|
||||
let computed_hash = std::hash::blake3(hash_input);
|
||||
let correct_hash = computed_hash == committed_hash;
|
||||
println(f"Correct hash? {correct_hash}");
|
||||
correct_hash
|
||||
}
|
||||
|
||||
fn make_hash_input(dob: str<10>, blinder: [u8; 16]) -> [u8; 26] {
|
||||
let mut input: [u8; 26] = [0; 26];
|
||||
for i in 0..10 {
|
||||
input[i] = dob.as_bytes()[i];
|
||||
}
|
||||
for i in 0..16 {
|
||||
input[10 + i] = blinder[i];
|
||||
}
|
||||
input
|
||||
}
|
||||
|
||||
pub fn parse_birth_date(birth_date: str<10>) -> date::Date {
|
||||
let date: [u8; 10] = birth_date.as_bytes();
|
||||
let date_str: str<8> =
|
||||
[date[0], date[1], date[2], date[3], date[5], date[6], date[8], date[9]].as_str_unchecked();
|
||||
Date::from_str_long_year(date_str)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_is_over_18() {
|
||||
// Private input
|
||||
let date_of_birth = "1985-03-12";
|
||||
let blinder = [109, 224, 222, 179, 60, 44, 41, 65, 166, 94, 111, 216, 73, 231, 63, 83];
|
||||
|
||||
// Public input
|
||||
let proof_date = date::Date { year: 2025, month: 9, day: 26 };
|
||||
let committed_hash = [
|
||||
114, 34, 41, 235, 91, 156, 13, 57, 254, 112, 250, 35, 104, 217, 20, 182, 240, 170, 57, 39,
|
||||
187, 154, 14, 39, 91, 67, 50, 199, 149, 231, 78, 46,
|
||||
];
|
||||
|
||||
main(proof_date, committed_hash, date_of_birth, blinder);
|
||||
}
|
||||
|
||||
#[test(should_fail)]
|
||||
fn test_under_18() {
|
||||
// Private input
|
||||
let date_of_birth = "2010-08-01";
|
||||
let blinder = [160, 23, 57, 158, 141, 195, 155, 132, 109, 242, 48, 220, 70, 217, 229, 189];
|
||||
|
||||
// Public input
|
||||
let proof_date = date::Date { year: 2025, month: 8, day: 29 };
|
||||
let committed_hash = [
|
||||
16, 132, 194, 62, 232, 90, 157, 153, 4, 231, 1, 54, 226, 3, 87, 174, 129, 177, 80, 69, 37,
|
||||
222, 209, 91, 168, 156, 9, 109, 108, 144, 168, 109,
|
||||
];
|
||||
|
||||
main(proof_date, committed_hash, date_of_birth, blinder);
|
||||
}
|
||||
1
crates/examples/interactive_zk/noir/target/noir.json
Normal file
1
crates/examples/interactive_zk/noir/target/noir.json
Normal file
File diff suppressed because one or more lines are too long
379
crates/examples/interactive_zk/prover.rs
Normal file
379
crates/examples/interactive_zk/prover.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::types::received_commitments;
|
||||
|
||||
use super::types::ZKProofBundle;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use http_body_util::Empty;
|
||||
use hyper::{body::Bytes, header, Request, StatusCode, Uri};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use k256::sha2::Digest;
|
||||
use noir::{
|
||||
barretenberg::{
|
||||
prove::prove_ultra_honk, srs::setup_srs_from_bytecode,
|
||||
verify::get_ultra_honk_verification_key,
|
||||
},
|
||||
witness::from_vec_str_to_witness_map,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use spansy::{
|
||||
http::{BodyContent, Requests, Responses},
|
||||
Spanned,
|
||||
};
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::{
|
||||
config::{CertificateDer, ProtocolConfig, RootCertStore},
|
||||
connection::ServerName,
|
||||
hash::HashAlgId,
|
||||
prover::{ProveConfig, ProveConfigBuilder, Prover, ProverConfig, TlsConfig},
|
||||
transcript::{
|
||||
hash::{PlaintextHash, PlaintextHashSecret},
|
||||
Direction, TranscriptCommitConfig, TranscriptCommitConfigBuilder, TranscriptCommitmentKind,
|
||||
TranscriptSecret,
|
||||
},
|
||||
};
|
||||
|
||||
use tlsn_examples::MAX_RECV_DATA;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use tlsn_examples::MAX_SENT_DATA;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(verifier_socket, verifier_extra_socket))]
|
||||
pub async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
verifier_socket: T,
|
||||
mut verifier_extra_socket: T,
|
||||
server_addr: &SocketAddr,
|
||||
uri: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let uri = uri.parse::<Uri>()?;
|
||||
|
||||
if uri.scheme().map(|s| s.as_str()) != Some("https") {
|
||||
return Err("URI must use HTTPS scheme".into());
|
||||
}
|
||||
|
||||
let server_domain = uri.authority().ok_or("URI must have authority")?.host();
|
||||
|
||||
// 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 tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
});
|
||||
let tls_config = tls_config_builder.build()?;
|
||||
|
||||
// Set up protocol configuration for prover.
|
||||
let mut prover_config_builder = ProverConfig::builder();
|
||||
prover_config_builder
|
||||
.server_name(ServerName::Dns(server_domain.try_into()?))
|
||||
.tls_config(tls_config)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()?,
|
||||
);
|
||||
|
||||
let prover_config = prover_config_builder.build()?;
|
||||
|
||||
// Create prover and connect to verifier.
|
||||
//
|
||||
// Perform the setup phase with the verifier.
|
||||
let prover = Prover::new(prover_config)
|
||||
.setup(verifier_socket.compat())
|
||||
.await?;
|
||||
|
||||
// Connect to TLS Server.
|
||||
let tls_client_socket = tokio::net::TcpStream::connect(server_addr).await?;
|
||||
|
||||
// Pass server connection into the prover.
|
||||
let (mpc_tls_connection, prover_fut) = prover.connect(tls_client_socket.compat()).await?;
|
||||
|
||||
// Wrap the connection in a TokioIo compatibility layer to use it with hyper.
|
||||
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
|
||||
|
||||
// Spawn the Prover to run in the background.
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// MPC-TLS Handshake.
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection).await?;
|
||||
|
||||
// Spawn the connection to run in the background.
|
||||
tokio::spawn(connection);
|
||||
|
||||
// MPC-TLS: Send Request and wait for Response.
|
||||
let request = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.header("Host", server_domain)
|
||||
.header("Connection", "close")
|
||||
.header(header::AUTHORIZATION, "Bearer random_auth_token")
|
||||
.method("GET")
|
||||
.body(Empty::<Bytes>::new())?;
|
||||
|
||||
let response = request_sender.send_request(request).await?;
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err(format!("MPC-TLS request failed with status {}", response.status()).into());
|
||||
}
|
||||
|
||||
// Create proof for the Verifier.
|
||||
let mut prover = prover_task.await??;
|
||||
|
||||
let transcript = prover.transcript().clone();
|
||||
let mut prove_config_builder = ProveConfig::builder(&transcript);
|
||||
|
||||
// Reveal the DNS name.
|
||||
prove_config_builder.server_identity();
|
||||
|
||||
let sent: &[u8] = transcript.sent();
|
||||
let received: &[u8] = transcript.received();
|
||||
let sent_len = sent.len();
|
||||
let recv_len = received.len();
|
||||
tracing::info!("Sent length: {}, Received length: {}", sent_len, recv_len);
|
||||
|
||||
// Reveal the entire HTTP request except for the authorization bearer token
|
||||
reveal_request(sent, &mut prove_config_builder)?;
|
||||
|
||||
// Create hash commitment for the date of birth field from the response
|
||||
let mut transcript_commitment_builder = TranscriptCommitConfig::builder(&transcript);
|
||||
transcript_commitment_builder.default_kind(TranscriptCommitmentKind::Hash {
|
||||
alg: HashAlgId::BLAKE3,
|
||||
});
|
||||
reveal_received(
|
||||
received,
|
||||
&mut prove_config_builder,
|
||||
&mut transcript_commitment_builder,
|
||||
)?;
|
||||
|
||||
let transcripts_commitment_config = transcript_commitment_builder.build()?;
|
||||
prove_config_builder.transcript_commit(transcripts_commitment_config);
|
||||
|
||||
let prove_config = prove_config_builder.build()?;
|
||||
|
||||
// MPC-TLS prove
|
||||
let prover_output = prover.prove(&prove_config).await?;
|
||||
prover.close().await?;
|
||||
|
||||
// Prove birthdate is more than 18 years ago.
|
||||
let received_commitments = received_commitments(&prover_output.transcript_commitments);
|
||||
let received_commitment = received_commitments
|
||||
.first()
|
||||
.ok_or("No received commitments found")?; // committed hash (of date of birth string)
|
||||
let received_secrets = received_secrets(&prover_output.transcript_secrets);
|
||||
let received_secret = received_secrets
|
||||
.first()
|
||||
.ok_or("No received secrets found")?; // hash blinder
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let proof_input = prepare_zk_proof_input(received, received_commitment, received_secret)?;
|
||||
let prepare_duration = start_time.elapsed();
|
||||
tracing::info!("🔢 prepare_zk_proof_input took: {:?}", prepare_duration);
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let proof_bundle = generate_zk_proof(&proof_input)?;
|
||||
let generate_duration = start_time.elapsed();
|
||||
tracing::info!("⚡ generate_zk_proof took: {:?}", generate_duration);
|
||||
|
||||
// Sent zk proof bundle to verifier
|
||||
let serialized_proof = bincode::serialize(&proof_bundle)?;
|
||||
verifier_extra_socket.write_all(&serialized_proof).await?;
|
||||
verifier_extra_socket.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Reveal everything from the request, except for the authorization token.
|
||||
fn reveal_request(
|
||||
request: &[u8],
|
||||
builder: &mut ProveConfigBuilder<'_>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let reqs = Requests::new_from_slice(request).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let req = reqs.first().ok_or("No requests found")?;
|
||||
|
||||
if req.request.method.as_str() != "GET" {
|
||||
return Err(format!("Expected GET method, found {}", req.request.method.as_str()).into());
|
||||
}
|
||||
|
||||
let authorization_header = req
|
||||
.headers_with_name(header::AUTHORIZATION.as_str())
|
||||
.next()
|
||||
.ok_or("Authorization header not found")?;
|
||||
|
||||
let start_pos = authorization_header
|
||||
.span()
|
||||
.indices()
|
||||
.min()
|
||||
.ok_or("Could not find authorization header start position")?
|
||||
+ header::AUTHORIZATION.as_str().len()
|
||||
+ 2;
|
||||
let end_pos =
|
||||
start_pos + authorization_header.span().len() - header::AUTHORIZATION.as_str().len() - 2;
|
||||
|
||||
builder.reveal_sent(&(0..start_pos))?;
|
||||
builder.reveal_sent(&(end_pos..request.len()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reveal_received(
|
||||
received: &[u8],
|
||||
builder: &mut ProveConfigBuilder<'_>,
|
||||
transcript_commitment_builder: &mut TranscriptCommitConfigBuilder,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let resp = Responses::new_from_slice(received).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let response = resp.first().ok_or("No responses found")?;
|
||||
let body = response.body.as_ref().ok_or("Response body not found")?;
|
||||
|
||||
let BodyContent::Json(json) = &body.content else {
|
||||
return Err("Expected JSON body content".into());
|
||||
};
|
||||
|
||||
// reveal tax year
|
||||
let tax_year = json
|
||||
.get("tax_year")
|
||||
.ok_or("tax_year field not found in JSON")?;
|
||||
let start_pos = tax_year
|
||||
.span()
|
||||
.indices()
|
||||
.min()
|
||||
.ok_or("Could not find tax_year start position")?
|
||||
- 11;
|
||||
let end_pos = tax_year
|
||||
.span()
|
||||
.indices()
|
||||
.max()
|
||||
.ok_or("Could not find tax_year end position")?
|
||||
+ 1;
|
||||
builder.reveal_recv(&(start_pos..end_pos))?;
|
||||
|
||||
// commit to hash of date of birth
|
||||
let dob = json
|
||||
.get("taxpayer.date_of_birth")
|
||||
.ok_or("taxpayer.date_of_birth field not found in JSON")?;
|
||||
|
||||
transcript_commitment_builder.commit_recv(dob.span())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// extract secret from prover output
|
||||
fn received_secrets(transcript_secrets: &[TranscriptSecret]) -> Vec<&PlaintextHashSecret> {
|
||||
transcript_secrets
|
||||
.iter()
|
||||
.filter_map(|secret| match secret {
|
||||
TranscriptSecret::Hash(hash) if hash.direction == Direction::Received => Some(hash),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ZKProofInput {
|
||||
dob: Vec<u8>,
|
||||
proof_date: NaiveDate,
|
||||
blinder: Vec<u8>,
|
||||
committed_hash: Vec<u8>,
|
||||
}
|
||||
|
||||
// Verify that the blinded, committed hash is correct
|
||||
fn prepare_zk_proof_input(
|
||||
received: &[u8],
|
||||
received_commitment: &PlaintextHash,
|
||||
received_secret: &PlaintextHashSecret,
|
||||
) -> Result<ZKProofInput, Box<dyn std::error::Error>> {
|
||||
assert_eq!(received_commitment.direction, Direction::Received);
|
||||
assert_eq!(received_commitment.hash.alg, HashAlgId::BLAKE3);
|
||||
|
||||
let hash = &received_commitment.hash;
|
||||
|
||||
let dob_start = received_commitment
|
||||
.idx
|
||||
.min()
|
||||
.ok_or("No start index for DOB")?;
|
||||
let dob_end = received_commitment
|
||||
.idx
|
||||
.end()
|
||||
.ok_or("No end index for DOB")?;
|
||||
let dob = received[dob_start..dob_end].to_vec();
|
||||
let blinder = received_secret.blinder.as_bytes().to_vec();
|
||||
let committed_hash = hash.value.as_bytes().to_vec();
|
||||
let proof_date = Local::now().date_naive();
|
||||
|
||||
assert_eq!(received_secret.direction, Direction::Received);
|
||||
assert_eq!(received_secret.alg, HashAlgId::BLAKE3);
|
||||
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(&dob);
|
||||
hasher.update(&blinder);
|
||||
let computed_hash = hasher.finalize().as_bytes().to_vec();
|
||||
|
||||
if committed_hash != computed_hash.as_slice() {
|
||||
return Err("Computed hash does not match committed hash".into());
|
||||
}
|
||||
|
||||
Ok(ZKProofInput {
|
||||
dob,
|
||||
proof_date,
|
||||
committed_hash,
|
||||
blinder,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_zk_proof(
|
||||
proof_input: &ZKProofInput,
|
||||
) -> Result<ZKProofBundle, Box<dyn std::error::Error>> {
|
||||
tracing::info!("🔒 Generating ZK proof with Noir...");
|
||||
|
||||
const PROGRAM_JSON: &str = include_str!("./noir/target/noir.json");
|
||||
|
||||
// 1. Load bytecode from program.json
|
||||
let json: Value = serde_json::from_str(PROGRAM_JSON)?;
|
||||
let bytecode = json["bytecode"]
|
||||
.as_str()
|
||||
.ok_or("bytecode field not found in program.json")?;
|
||||
|
||||
let mut inputs: Vec<String> = vec![];
|
||||
inputs.push(proof_input.proof_date.day().to_string());
|
||||
inputs.push(proof_input.proof_date.month().to_string());
|
||||
inputs.push(proof_input.proof_date.year().to_string());
|
||||
inputs.extend(proof_input.committed_hash.iter().map(|b| b.to_string()));
|
||||
inputs.extend(proof_input.dob.iter().map(|b| b.to_string()));
|
||||
inputs.extend(proof_input.blinder.iter().map(|b| b.to_string()));
|
||||
|
||||
let proof_date = proof_input.proof_date.to_string();
|
||||
tracing::info!(
|
||||
"Public inputs : Proof date ({}) and committed hash ({})",
|
||||
proof_date,
|
||||
hex::encode(&proof_input.committed_hash)
|
||||
);
|
||||
tracing::info!(
|
||||
"Private inputs: Blinder ({}) and Date of Birth ({})",
|
||||
hex::encode(&proof_input.blinder),
|
||||
String::from_utf8_lossy(&proof_input.dob)
|
||||
);
|
||||
|
||||
tracing::debug!("Witness inputs {:?}", inputs);
|
||||
|
||||
let input_refs: Vec<&str> = inputs.iter().map(String::as_str).collect();
|
||||
let witness = from_vec_str_to_witness_map(input_refs)?;
|
||||
|
||||
// Setup SRS
|
||||
setup_srs_from_bytecode(bytecode, None, false)?;
|
||||
|
||||
// Verification key
|
||||
let vk = get_ultra_honk_verification_key(bytecode, false)?;
|
||||
|
||||
// Generate proof
|
||||
let proof = prove_ultra_honk(bytecode, witness.clone(), vk.clone(), false)?;
|
||||
tracing::info!("✅ Proof generated ({} bytes)", proof.len());
|
||||
|
||||
let proof_bundle = ZKProofBundle { vk, proof };
|
||||
Ok(proof_bundle)
|
||||
}
|
||||
21
crates/examples/interactive_zk/types.rs
Normal file
21
crates/examples/interactive_zk/types.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tlsn::transcript::{hash::PlaintextHash, Direction, TranscriptCommitment};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ZKProofBundle {
|
||||
pub vk: Vec<u8>,
|
||||
pub proof: Vec<u8>,
|
||||
}
|
||||
|
||||
// extract commitment from prover output
|
||||
pub fn received_commitments(
|
||||
transcript_commitments: &[TranscriptCommitment],
|
||||
) -> Vec<&PlaintextHash> {
|
||||
transcript_commitments
|
||||
.iter()
|
||||
.filter_map(|commitment| match commitment {
|
||||
TranscriptCommitment::Hash(hash) if hash.direction == Direction::Received => Some(hash),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
184
crates/examples/interactive_zk/verifier.rs
Normal file
184
crates/examples/interactive_zk/verifier.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use crate::types::received_commitments;
|
||||
|
||||
use super::types::ZKProofBundle;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use noir::barretenberg::verify::{get_ultra_honk_verification_key, verify_ultra_honk};
|
||||
use serde_json::Value;
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::{
|
||||
config::{CertificateDer, ProtocolConfigValidator, RootCertStore},
|
||||
connection::ServerName,
|
||||
hash::HashAlgId,
|
||||
transcript::{Direction, PartialTranscript},
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
use tlsn_examples::{MAX_RECV_DATA, MAX_SENT_DATA};
|
||||
use tlsn_server_fixture_certs::SERVER_DOMAIN;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(socket, extra_socket))]
|
||||
pub async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
socket: T,
|
||||
mut extra_socket: T,
|
||||
) -> Result<PartialTranscript, Box<dyn std::error::Error>> {
|
||||
// Set up Verifier.
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()?;
|
||||
|
||||
// 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 verifier_config = VerifierConfig::builder()
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()?;
|
||||
|
||||
let verifier = Verifier::new(verifier_config);
|
||||
|
||||
// Receive authenticated data.
|
||||
let VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
transcript_commitments,
|
||||
..
|
||||
} = verifier
|
||||
.verify(socket.compat(), &VerifyConfig::default())
|
||||
.await?;
|
||||
|
||||
let server_name = server_name.ok_or("Prover should have revealed server name")?;
|
||||
let transcript = transcript.ok_or("Prover should have revealed transcript data")?;
|
||||
|
||||
// Create hash commitment for the date of birth field from the response
|
||||
let sent = transcript.sent_unsafe().to_vec();
|
||||
let sent_data = String::from_utf8(sent.clone())
|
||||
.map_err(|e| format!("Verifier expected valid UTF-8 sent data: {}", e))?;
|
||||
|
||||
if !sent_data.contains(SERVER_DOMAIN) {
|
||||
return Err(format!(
|
||||
"Verification failed: Expected host {} not found in sent data",
|
||||
SERVER_DOMAIN
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check received data.
|
||||
let received_commitments = received_commitments(&transcript_commitments);
|
||||
let received_commitment = received_commitments
|
||||
.first()
|
||||
.ok_or("Missing received hash commitment")?;
|
||||
|
||||
assert!(received_commitment.direction == Direction::Received);
|
||||
assert!(received_commitment.hash.alg == HashAlgId::BLAKE3);
|
||||
|
||||
let committed_hash = &received_commitment.hash;
|
||||
|
||||
// Check Session info: server name.
|
||||
let ServerName::Dns(server_name) = server_name;
|
||||
if server_name.as_str() != SERVER_DOMAIN {
|
||||
return Err(format!(
|
||||
"Server name mismatch: expected {}, got {}",
|
||||
SERVER_DOMAIN,
|
||||
server_name.as_str()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Receive ZKProof information from prover
|
||||
let mut buf = Vec::new();
|
||||
extra_socket.read_to_end(&mut buf).await?;
|
||||
|
||||
if buf.is_empty() {
|
||||
return Err("No ZK proof data received from prover".into());
|
||||
}
|
||||
|
||||
let msg: ZKProofBundle = bincode::deserialize(&buf)
|
||||
.map_err(|e| format!("Failed to deserialize ZK proof bundle: {}", e))?;
|
||||
|
||||
// Verify zk proof
|
||||
const PROGRAM_JSON: &str = include_str!("./noir/target/noir.json");
|
||||
let json: Value = serde_json::from_str(PROGRAM_JSON)
|
||||
.map_err(|e| format!("Failed to parse Noir circuit: {}", e))?;
|
||||
|
||||
let bytecode = json["bytecode"]
|
||||
.as_str()
|
||||
.ok_or("Bytecode field missing in noir.json")?;
|
||||
|
||||
let vk = get_ultra_honk_verification_key(bytecode, false)
|
||||
.map_err(|e| format!("Failed to get verification key: {}", e))?;
|
||||
|
||||
if vk != msg.vk {
|
||||
return Err("Verification key mismatch between computed and provided by prover".into());
|
||||
}
|
||||
|
||||
let proof = msg.proof.clone();
|
||||
|
||||
// Validate proof has enough data.
|
||||
// The proof should start with the public inputs:
|
||||
// * We expect at least 3 * 32 bytes for the three date fields (day, month,
|
||||
// year)
|
||||
// * and 32*32 bytes for the hash
|
||||
let min_bytes = (32 + 3) * 32;
|
||||
if proof.len() < min_bytes {
|
||||
return Err(format!(
|
||||
"Proof too short: expected at least {} bytes, got {}",
|
||||
min_bytes,
|
||||
proof.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check that the proof date is correctly included in the proof
|
||||
let proof_date_day: u32 = u32::from_be_bytes(proof[28..32].try_into()?);
|
||||
let proof_date_month: u32 = u32::from_be_bytes(proof[60..64].try_into()?);
|
||||
let proof_date_year: i32 = i32::from_be_bytes(proof[92..96].try_into()?);
|
||||
let proof_date_from_proof =
|
||||
NaiveDate::from_ymd_opt(proof_date_year, proof_date_month, proof_date_day)
|
||||
.ok_or("Invalid proof date in proof")?;
|
||||
let today = Local::now().date_naive();
|
||||
if (today - proof_date_from_proof).num_days() < 0 {
|
||||
return Err(format!(
|
||||
"The proof date can only be today or in the past: provided {}, today {}",
|
||||
proof_date_from_proof, today
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check that the committed hash in the proof matches the hash from the
|
||||
// commitment
|
||||
let committed_hash_in_proof: Vec<u8> = proof
|
||||
.chunks(32)
|
||||
.skip(3) // skip the first 3 chunks
|
||||
.take(32)
|
||||
.map(|chunk| *chunk.last().unwrap_or(&0))
|
||||
.collect();
|
||||
let expected_hash = committed_hash.value.as_bytes().to_vec();
|
||||
if committed_hash_in_proof != expected_hash {
|
||||
tracing::error!(
|
||||
"❌ The hash in the proof does not match the committed hash in MPC-TLS: {} != {}",
|
||||
hex::encode(&committed_hash_in_proof),
|
||||
hex::encode(&expected_hash)
|
||||
);
|
||||
return Err("Hash in proof does not match committed hash in MPC-TLS".into());
|
||||
}
|
||||
tracing::info!(
|
||||
"✅ The hash in the proof matches the committed hash in MPC-TLS ({})",
|
||||
hex::encode(&expected_hash)
|
||||
);
|
||||
|
||||
// Finally verify the proof
|
||||
let is_valid = verify_ultra_honk(msg.proof, msg.vk)
|
||||
.map_err(|e| format!("ZKProof Verification failed: {}", e))?;
|
||||
if !is_valid {
|
||||
tracing::error!("❌ Age verification ZKProof failed to verify");
|
||||
return Err("Age verification ZKProof failed to verify".into());
|
||||
}
|
||||
tracing::info!("✅ Age verification ZKProof successfully verified");
|
||||
|
||||
Ok(transcript)
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub enum Id {
|
||||
One,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub enum IoMode {
|
||||
Client,
|
||||
Server,
|
||||
|
||||
@@ -7,12 +7,12 @@ 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"
|
||||
docker run -it --privileged -v $(pwd)/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"
|
||||
docker run -it --privileged -v $(pwd)/crates/harness/:/benches tlsn-bench bash -c "runner setup; runner --target browser bench"
|
||||
```
|
||||
|
||||
|
||||
@@ -8,14 +8,9 @@ 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 = { workspace = true }
|
||||
tlsn-core = { workspace = true }
|
||||
tlsn-tls-core = { workspace = true }
|
||||
tlsn-server-fixture-certs = { workspace = true }
|
||||
|
||||
inventory = { workspace = true }
|
||||
@@ -33,6 +28,8 @@ tokio = { workspace = true, features = ["full"] }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# Disable tracing events as a workaround for issue 959.
|
||||
tracing = { workspace = true, features = ["release_max_level_off"] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
tlsn-wasm = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
|
||||
@@ -5,7 +5,8 @@ use futures::{AsyncReadExt, AsyncWriteExt, TryFutureExt};
|
||||
|
||||
use harness_core::bench::{Bench, ProverMetrics};
|
||||
use tlsn::{
|
||||
config::ProtocolConfig,
|
||||
config::{CertificateDer, ProtocolConfig, RootCertStore},
|
||||
connection::ServerName,
|
||||
prover::{ProveConfig, Prover, ProverConfig, TlsConfig},
|
||||
};
|
||||
use tlsn_server_fixture_certs::{CA_CERT_DER, SERVER_DOMAIN};
|
||||
@@ -32,20 +33,17 @@ pub async fn bench_prover(provider: &IoProvider, config: &Bench) -> Result<Prove
|
||||
|
||||
let protocol_config = builder.build()?;
|
||||
|
||||
let mut root_store = tls_core::anchors::RootCertStore::empty();
|
||||
root_store
|
||||
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
|
||||
.unwrap();
|
||||
|
||||
let mut tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder.root_store(root_store);
|
||||
tls_config_builder.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
});
|
||||
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)
|
||||
.server_name(ServerName::Dns(SERVER_DOMAIN.try_into().unwrap()))
|
||||
.build()?,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
|
||||
use harness_core::bench::Bench;
|
||||
use tlsn::{
|
||||
config::ProtocolConfigValidator,
|
||||
config::{CertificateDer, ProtocolConfigValidator, RootCertStore},
|
||||
verifier::{Verifier, VerifierConfig, VerifyConfig},
|
||||
};
|
||||
use tlsn_server_fixture_certs::CA_CERT_DER;
|
||||
@@ -17,14 +17,11 @@ pub async fn bench_verifier(provider: &IoProvider, config: &Bench) -> Result<()>
|
||||
|
||||
let protocol_config = builder.build()?;
|
||||
|
||||
let mut root_store = tls_core::anchors::RootCertStore::empty();
|
||||
root_store
|
||||
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
|
||||
.unwrap();
|
||||
|
||||
let verifier = Verifier::new(
|
||||
VerifierConfig::builder()
|
||||
.root_store(root_store)
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.protocol_config_validator(protocol_config)
|
||||
.build()?,
|
||||
);
|
||||
|
||||
@@ -81,7 +81,11 @@ mod native {
|
||||
mod wasm {
|
||||
use super::IoProvider;
|
||||
use crate::io::Io;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::time::Duration;
|
||||
|
||||
const CHECK_WS_OPEN_DELAY_MS: usize = 50;
|
||||
const MAX_RETRIES: usize = 50;
|
||||
|
||||
impl IoProvider {
|
||||
/// Provides a connection to the server.
|
||||
@@ -107,7 +111,27 @@ mod wasm {
|
||||
&self.config.proto_1.0,
|
||||
self.config.proto_1.1,
|
||||
);
|
||||
let (_, io) = ws_stream_wasm::WsMeta::connect(url, None).await?;
|
||||
let mut retries = 0;
|
||||
|
||||
let io = loop {
|
||||
// Connect to the websocket relay.
|
||||
let (_, io) = ws_stream_wasm::WsMeta::connect(url.clone(), None).await?;
|
||||
|
||||
// Allow some time for the relay to initiate a connection to
|
||||
// the verifier.
|
||||
std::thread::sleep(Duration::from_millis(CHECK_WS_OPEN_DELAY_MS as u64));
|
||||
|
||||
// If the relay didn't close the io, most likely the verifier
|
||||
// accepted the connection.
|
||||
if io.ready_state() == ws_stream_wasm::WsState::Open {
|
||||
break io;
|
||||
}
|
||||
|
||||
retries += 1;
|
||||
if retries > MAX_RETRIES {
|
||||
return Err(anyhow!("verifier did not accept connection"));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(io.into_io())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tls_core::anchors::RootCertStore;
|
||||
use tlsn::{
|
||||
config::{ProtocolConfig, ProtocolConfigValidator},
|
||||
config::{CertificateDer, ProtocolConfig, ProtocolConfigValidator, RootCertStore},
|
||||
connection::ServerName,
|
||||
hash::HashAlgId,
|
||||
prover::{ProveConfig, Prover, ProverConfig, TlsConfig},
|
||||
transcript::{TranscriptCommitConfig, TranscriptCommitment, TranscriptCommitmentKind},
|
||||
@@ -21,19 +21,17 @@ const MAX_RECV_DATA: usize = 1 << 11;
|
||||
crate::test!("basic", prover, verifier);
|
||||
|
||||
async fn prover(provider: &IoProvider) {
|
||||
let mut root_store = RootCertStore::empty();
|
||||
root_store
|
||||
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
|
||||
.unwrap();
|
||||
|
||||
let mut tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder.root_store(root_store);
|
||||
tls_config_builder.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
});
|
||||
|
||||
let tls_config = tls_config_builder.build().unwrap();
|
||||
|
||||
let server_name = ServerName::Dns(SERVER_DOMAIN.try_into().unwrap());
|
||||
let prover = Prover::new(
|
||||
ProverConfig::builder()
|
||||
.server_name(SERVER_DOMAIN)
|
||||
.server_name(server_name)
|
||||
.tls_config(tls_config)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
@@ -114,11 +112,6 @@ async fn prover(provider: &IoProvider) {
|
||||
}
|
||||
|
||||
async fn verifier(provider: &IoProvider) {
|
||||
let mut root_store = RootCertStore::empty();
|
||||
root_store
|
||||
.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))
|
||||
.unwrap();
|
||||
|
||||
let config = VerifierConfig::builder()
|
||||
.protocol_config_validator(
|
||||
ProtocolConfigValidator::builder()
|
||||
@@ -127,7 +120,9 @@ async fn verifier(provider: &IoProvider) {
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.root_store(root_store)
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -145,7 +140,9 @@ async fn verifier(provider: &IoProvider) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(server_name.unwrap().as_str(), SERVER_DOMAIN);
|
||||
let ServerName::Dns(server_name) = server_name.unwrap();
|
||||
|
||||
assert_eq!(server_name.as_str(), SERVER_DOMAIN);
|
||||
assert!(
|
||||
transcript_commitments
|
||||
.iter()
|
||||
|
||||
@@ -8,6 +8,7 @@ use chromiumoxide::{
|
||||
network::{EnableParams, SetCacheDisabledParams},
|
||||
page::ReloadParams,
|
||||
},
|
||||
handler::HandlerConfig,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use harness_core::{
|
||||
@@ -126,8 +127,18 @@ impl Executor {
|
||||
const TIMEOUT: usize = 10000;
|
||||
const DELAY: usize = 100;
|
||||
let mut retries = 0;
|
||||
let config = HandlerConfig {
|
||||
// Bump the timeout for long-running benches.
|
||||
request_timeout: Duration::from_secs(120),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (browser, mut handler) = loop {
|
||||
match Browser::connect(format!("http://{}:{}", rpc_addr.0, PORT_BROWSER)).await
|
||||
match Browser::connect_with_config(
|
||||
format!("http://{}:{}", rpc_addr.0, PORT_BROWSER),
|
||||
config.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(browser) => break browser,
|
||||
Err(e) => {
|
||||
@@ -143,6 +154,14 @@ impl Executor {
|
||||
tokio::spawn(async move {
|
||||
while let Some(res) = handler.next().await {
|
||||
if let Err(e) = res {
|
||||
if e.to_string()
|
||||
== "data did not match any variant of untagged enum Message"
|
||||
{
|
||||
// Do not log this error. It appears to be
|
||||
// caused by a bug upstream.
|
||||
// https://github.com/mattsse/chromiumoxide/issues/167
|
||||
continue;
|
||||
}
|
||||
eprintln!("chromium error: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,3 +72,5 @@ tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
tracing-subscriber = { workspace = true }
|
||||
uid-mux = { workspace = true, features = ["serio", "test-utils"] }
|
||||
rustls-pki-types = { workspace = true }
|
||||
rustls-webpki = { workspace = true }
|
||||
|
||||
@@ -22,7 +22,7 @@ use serio::stream::IoStreamExt;
|
||||
use std::mem;
|
||||
use tls_core::msgs::enums::NamedGroup;
|
||||
use tlsn_core::{
|
||||
connection::{HandshakeData, HandshakeDataV1_2, TlsVersion, VerifyData},
|
||||
connection::{CertBinding, CertBindingV1_2, TlsVersion, VerifyData},
|
||||
transcript::TlsTranscript,
|
||||
};
|
||||
use tracing::{debug, instrument};
|
||||
@@ -405,7 +405,7 @@ impl MpcTlsFollower {
|
||||
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"))?;
|
||||
|
||||
let handshake_data = HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let handshake_data = CertBinding::V1_2(CertBindingV1_2 {
|
||||
client_random,
|
||||
server_random,
|
||||
server_ephemeral_key: server_key
|
||||
|
||||
@@ -43,10 +43,9 @@ use tls_core::{
|
||||
suites::SupportedCipherSuite,
|
||||
};
|
||||
use tlsn_core::{
|
||||
connection::{
|
||||
Certificate, HandshakeData, HandshakeDataV1_2, ServerSignature, TlsVersion, VerifyData,
|
||||
},
|
||||
connection::{CertBinding, CertBindingV1_2, ServerSignature, TlsVersion, VerifyData},
|
||||
transcript::TlsTranscript,
|
||||
webpki::CertificateDer,
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
@@ -325,7 +324,7 @@ impl MpcTlsLeader {
|
||||
let server_cert_chain = server_cert_details
|
||||
.cert_chain()
|
||||
.iter()
|
||||
.map(|cert| Certificate(cert.0.clone()))
|
||||
.map(|cert| CertificateDer(cert.0.clone()))
|
||||
.collect();
|
||||
|
||||
let server_signature = ServerSignature {
|
||||
@@ -337,7 +336,7 @@ impl MpcTlsLeader {
|
||||
sig: server_kx_details.kx_sig().sig.0.clone(),
|
||||
};
|
||||
|
||||
let handshake_data = HandshakeData::V1_2(HandshakeDataV1_2 {
|
||||
let handshake_data = CertBinding::V1_2(CertBindingV1_2 {
|
||||
client_random: client_random.0,
|
||||
server_random: server_random.0,
|
||||
server_ephemeral_key: server_key
|
||||
|
||||
@@ -72,4 +72,5 @@ pub(crate) struct ServerFinishedVd {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct CloseConnection;
|
||||
|
||||
@@ -193,7 +193,7 @@ where
|
||||
};
|
||||
|
||||
// Divide by block length and round up.
|
||||
let block_count = input.len() / 16 + (input.len() % 16 != 0) as usize;
|
||||
let block_count = input.len() / 16 + !input.len().is_multiple_of(16) as usize;
|
||||
|
||||
if block_count > MAX_POWER {
|
||||
return Err(ErrorRepr::InputLength {
|
||||
@@ -282,11 +282,11 @@ fn build_ghash_data(mut aad: Vec<u8>, mut ciphertext: Vec<u8>) -> Vec<u8> {
|
||||
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;
|
||||
let aad_padded_block_count = (aad.len() / 16) + !aad.len().is_multiple_of(16) 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.len() / 16) + !ciphertext.len().is_multiple_of(16) as usize;
|
||||
ciphertext.resize(ciphertext_padded_block_count * 16, 0);
|
||||
|
||||
let mut data: Vec<u8> = Vec::with_capacity(aad.len() + ciphertext.len() + 16);
|
||||
|
||||
@@ -12,11 +12,15 @@ use mpz_ot::{
|
||||
rcot::shared::{SharedRCOTReceiver, SharedRCOTSender},
|
||||
};
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
use tls_client::Certificate;
|
||||
use rustls_pki_types::CertificateDer;
|
||||
use tls_client::RootCertStore;
|
||||
use tls_client_async::bind_client;
|
||||
use tls_server_fixture::{bind_test_server_hyper, CA_CERT_DER, SERVER_DOMAIN};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use webpki::anchor_from_trusted_cert;
|
||||
|
||||
const CA_CERT: CertificateDer = CertificateDer::from_slice(CA_CERT_DER);
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "expensive"]
|
||||
@@ -48,11 +52,11 @@ async fn leader_task(mut leader: MpcTlsLeader) {
|
||||
let (leader_ctrl, leader_fut) = leader.run();
|
||||
tokio::spawn(async { leader_fut.await.unwrap() });
|
||||
|
||||
let mut root_store = tls_client::RootCertStore::empty();
|
||||
root_store.add(&Certificate(CA_CERT_DER.to_vec())).unwrap();
|
||||
let config = tls_client::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_root_certificates(RootCertStore {
|
||||
roots: vec![anchor_from_trusted_cert(&CA_CERT).unwrap().to_owned()],
|
||||
})
|
||||
.with_no_client_auth();
|
||||
|
||||
let server_name = SERVER_DOMAIN.try_into().unwrap();
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
[package]
|
||||
name = "notary-client"
|
||||
version = "0.1.0-alpha.13-pre"
|
||||
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 }
|
||||
@@ -1,530 +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(¬arization_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)
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "notary-common"
|
||||
version = "0.1.0-alpha.13-pre"
|
||||
description = "Common code shared between notary-server and notary-client"
|
||||
edition = "2021"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
[package]
|
||||
name = "notary-server"
|
||||
version = "0.1.0-alpha.13-pre"
|
||||
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 = { 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
|
||||
@@ -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.
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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" ]
|
||||
@@ -1,4 +0,0 @@
|
||||
# exclude Rust build artifacts
|
||||
./target
|
||||
./crates/wasm/pkg/
|
||||
./crates/harness/static/generated/
|
||||
@@ -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.13-pre
|
||||
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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
use eyre::Result;
|
||||
use jsonwebtoken::{Algorithm as JwtAlgorithm, DecodingKey};
|
||||
use serde_json::Value;
|
||||
use strum::{EnumString, VariantNames};
|
||||
use tracing::error;
|
||||
|
||||
use crate::JwtClaim;
|
||||
|
||||
/// Custom error for JWT handling
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
#[error("JWT validation error: {0}")]
|
||||
pub struct JwtValidationError(String);
|
||||
|
||||
type JwtResult<T> = std::result::Result<T, JwtValidationError>;
|
||||
|
||||
/// JWT config which also encapsulates claims validation logic.
|
||||
#[derive(Clone)]
|
||||
pub struct Jwt {
|
||||
pub algorithm: Algorithm,
|
||||
pub key: DecodingKey,
|
||||
pub claims: Vec<JwtClaim>,
|
||||
}
|
||||
|
||||
impl Jwt {
|
||||
pub fn validate(&self, claims: &Value) -> JwtResult<()> {
|
||||
Jwt::validate_claims(&self.claims, claims)
|
||||
}
|
||||
|
||||
fn validate_claims(expected: &[JwtClaim], claims: &Value) -> JwtResult<()> {
|
||||
expected
|
||||
.iter()
|
||||
.try_for_each(|expected| Self::validate_claim(expected, claims))
|
||||
}
|
||||
|
||||
fn validate_claim(expected: &JwtClaim, given: &Value) -> JwtResult<()> {
|
||||
let pointer = format!("/{}", expected.name.replace(".", "/"));
|
||||
let field = given.pointer(&pointer).ok_or(JwtValidationError(format!(
|
||||
"missing claim '{}'",
|
||||
expected.name
|
||||
)))?;
|
||||
|
||||
let field_typed = field.as_str().ok_or(JwtValidationError(format!(
|
||||
"unexpected type for claim '{}': only strings are supported for claim values",
|
||||
expected.name,
|
||||
)))?;
|
||||
if !expected.values.is_empty() {
|
||||
expected.values.iter().any(|exp| exp == field_typed).then_some(()).ok_or_else(|| {
|
||||
let expected_values = expected.values.iter().map(|x| format!("'{x}'")).collect::<Vec<String>>().join(", ");
|
||||
JwtValidationError(format!(
|
||||
"unexpected value for claim '{}': expected one of [ {expected_values} ], received '{field_typed}'", expected.name
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(EnumString, Debug, Clone, Copy, PartialEq, Eq, VariantNames)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
/// Supported JWT signing algorithms
|
||||
pub enum Algorithm {
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-256
|
||||
RS256,
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-384
|
||||
RS384,
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-512
|
||||
RS512,
|
||||
/// RSASSA-PSS using SHA-256
|
||||
PS256,
|
||||
/// RSASSA-PSS using SHA-384
|
||||
PS384,
|
||||
/// RSASSA-PSS using SHA-512
|
||||
PS512,
|
||||
/// ECDSA using SHA-256
|
||||
ES256,
|
||||
/// ECDSA using SHA-384
|
||||
ES384,
|
||||
/// Edwards-curve Digital Signature Algorithm (EdDSA)
|
||||
EdDSA,
|
||||
}
|
||||
|
||||
impl From<Algorithm> for JwtAlgorithm {
|
||||
fn from(value: Algorithm) -> Self {
|
||||
match value {
|
||||
Algorithm::RS256 => Self::RS256,
|
||||
Algorithm::RS384 => Self::RS384,
|
||||
Algorithm::RS512 => Self::RS512,
|
||||
Algorithm::PS256 => Self::PS256,
|
||||
Algorithm::PS384 => Self::PS384,
|
||||
Algorithm::PS512 => Self::PS512,
|
||||
Algorithm::ES256 => Self::ES256,
|
||||
Algorithm::ES384 => Self::ES384,
|
||||
Algorithm::EdDSA => Self::EdDSA,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load JWT public key
|
||||
pub(super) async fn load_jwt_key(
|
||||
public_key_pem_path: &str,
|
||||
algorithm: Algorithm,
|
||||
) -> Result<DecodingKey> {
|
||||
let key_pem_bytes = tokio::fs::read(public_key_pem_path).await?;
|
||||
let key = match algorithm {
|
||||
Algorithm::RS256
|
||||
| Algorithm::RS384
|
||||
| Algorithm::RS512
|
||||
| Algorithm::PS256
|
||||
| Algorithm::PS384
|
||||
| Algorithm::PS512 => DecodingKey::from_rsa_pem(&key_pem_bytes)?,
|
||||
Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_pem(&key_pem_bytes)?,
|
||||
Algorithm::EdDSA => DecodingKey::from_ed_pem(&key_pem_bytes)?,
|
||||
};
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn validates_presence() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"sub": "test",
|
||||
});
|
||||
Jwt::validate_claim(&expected, &given).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_expected_value() {
|
||||
let expected = JwtClaim {
|
||||
name: "custom.host".to_string(),
|
||||
values: vec!["tlsn.com".to_string(), "api.tlsn.com".to_string()],
|
||||
};
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"custom": {
|
||||
"host": "api.tlsn.com",
|
||||
},
|
||||
});
|
||||
Jwt::validate_claim(&expected, &given).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_with_unknown_claims() {
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"sub": "test",
|
||||
"what": "is_this",
|
||||
});
|
||||
Jwt::validate_claims(&[], &given).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_claim_missing() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"host": "localhost",
|
||||
});
|
||||
assert_eq!(
|
||||
Jwt::validate_claim(&expected, &given),
|
||||
Err(JwtValidationError("missing claim 'sub'".to_string()))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_claim_has_unknown_value() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
values: vec!["tlsn_prod".to_string(), "tlsn_test".to_string()],
|
||||
};
|
||||
let given = json!({
|
||||
"sub": "tlsn",
|
||||
});
|
||||
assert_eq!(
|
||||
Jwt::validate_claim(&expected, &given),
|
||||
Err(JwtValidationError("unexpected value for claim 'sub': expected one of [ 'tlsn_prod', 'tlsn_test' ], received 'tlsn'".to_string()))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_claim_has_invalid_value_type() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let given = json!({
|
||||
"sub": { "name": "john" }
|
||||
});
|
||||
assert_eq!(
|
||||
Jwt::validate_claim(&expected, &given),
|
||||
Err(JwtValidationError(
|
||||
"unexpected type for claim 'sub': only strings are supported for claim values"
|
||||
.to_string()
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
use eyre::{eyre, Result};
|
||||
use notify::{
|
||||
event::ModifyKind, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::util::parse_csv_file;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Whitelist {
|
||||
pub entries: Arc<Mutex<HashMap<String, AuthorizationWhitelistRecord>>>,
|
||||
pub csv_path: String,
|
||||
}
|
||||
|
||||
/// Structure of each whitelisted record of the API key whitelist for
|
||||
/// authorization purpose
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct AuthorizationWhitelistRecord {
|
||||
pub name: String,
|
||||
pub api_key: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// Convert whitelist data structure from vector to hashmap using api_key as the
|
||||
/// key to speed up lookup
|
||||
pub(crate) fn authorization_whitelist_vec_into_hashmap(
|
||||
authorization_whitelist: Vec<AuthorizationWhitelistRecord>,
|
||||
) -> HashMap<String, AuthorizationWhitelistRecord> {
|
||||
let mut hashmap = HashMap::new();
|
||||
authorization_whitelist.iter().for_each(|record| {
|
||||
hashmap.insert(record.api_key.clone(), record.to_owned());
|
||||
});
|
||||
hashmap
|
||||
}
|
||||
|
||||
/// Load authorization whitelist
|
||||
pub(super) fn load_authorization_whitelist(
|
||||
whitelist_csv_path: &str,
|
||||
) -> Result<HashMap<String, AuthorizationWhitelistRecord>> {
|
||||
// Load the csv
|
||||
let whitelist_csv = parse_csv_file::<AuthorizationWhitelistRecord>(whitelist_csv_path)
|
||||
.map_err(|err| eyre!("Failed to parse authorization whitelist csv: {:?}", err))?;
|
||||
// Convert the whitelist record into hashmap for faster lookup
|
||||
let whitelist_hashmap = authorization_whitelist_vec_into_hashmap(whitelist_csv);
|
||||
Ok(whitelist_hashmap)
|
||||
}
|
||||
|
||||
// Setup a watcher to detect any changes to authorization whitelist
|
||||
// When the list file is modified, the watcher thread will reload the whitelist
|
||||
// The watcher is setup in a separate thread by the notify library which is
|
||||
// synchronous
|
||||
pub fn watch_and_reload_authorization_whitelist(
|
||||
whitelist: &Whitelist,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
let whitelist_csv_path_cloned = whitelist.csv_path.clone();
|
||||
let entries = whitelist.entries.clone();
|
||||
// Setup watcher by giving it a function that will be triggered when an event is
|
||||
// detected
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |event: Result<Event, Error>| {
|
||||
match event {
|
||||
Ok(event) => {
|
||||
// Only reload whitelist if it's an event that modified the file data
|
||||
if let EventKind::Modify(ModifyKind::Data(_)) = event.kind {
|
||||
debug!("Authorization whitelist is modified");
|
||||
match load_authorization_whitelist(&whitelist_csv_path_cloned) {
|
||||
Ok(new_authorization_whitelist) => {
|
||||
*entries.lock().unwrap() = new_authorization_whitelist;
|
||||
info!("Successfully reloaded authorization whitelist!");
|
||||
}
|
||||
// Ensure that error from reloading doesn't bring the server down
|
||||
Err(err) => error!("{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error occured when watcher detected an event: {err}")
|
||||
}
|
||||
}
|
||||
},
|
||||
notify::Config::default(),
|
||||
)
|
||||
.map_err(|err| eyre!("Error occured when setting up watcher for hot reload: {err}"))?;
|
||||
|
||||
// Start watcher to listen to any changes on the whitelist file
|
||||
watcher
|
||||
.watch(Path::new(&whitelist.csv_path), RecursiveMode::Recursive)
|
||||
.map_err(|err| eyre!("Error occured when starting up watcher for hot reload: {err}"))?;
|
||||
|
||||
// Need to return the watcher to parent function, else it will be dropped and
|
||||
// stop listening
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs::OpenOptions, time::Duration};
|
||||
|
||||
use csv::WriterBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_watch_and_reload_authorization_whitelist() {
|
||||
// Clone fixture auth whitelist for testing
|
||||
let original_whitelist_csv_path = "../tests-integration/fixture/auth/whitelist.csv";
|
||||
let whitelist_csv_path =
|
||||
"../tests-integration/fixture/auth/whitelist_copied.csv".to_string();
|
||||
std::fs::copy(original_whitelist_csv_path, &whitelist_csv_path).unwrap();
|
||||
|
||||
// Setup watcher
|
||||
let entries = load_authorization_whitelist(&whitelist_csv_path).expect(
|
||||
"Authorization whitelist csv from fixture should be able
|
||||
to be loaded",
|
||||
);
|
||||
let whitelist = Whitelist {
|
||||
entries: Arc::new(Mutex::new(entries)),
|
||||
csv_path: whitelist_csv_path.clone(),
|
||||
};
|
||||
let _watcher = watch_and_reload_authorization_whitelist(&whitelist)
|
||||
.expect("Watcher should be able to be setup successfully");
|
||||
|
||||
// Sleep to buy a bit of time for hot reload task and watcher thread to run
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Write a new record to the whitelist to trigger modify event
|
||||
let new_record = AuthorizationWhitelistRecord {
|
||||
name: "unit-test-name".to_string(),
|
||||
api_key: "unit-test-api-key".to_string(),
|
||||
created_at: "unit-test-created-at".to_string(),
|
||||
};
|
||||
let file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&whitelist_csv_path)
|
||||
.unwrap();
|
||||
let mut wtr = WriterBuilder::new()
|
||||
.has_headers(false) // Set to false to avoid writing header again
|
||||
.from_writer(file);
|
||||
wtr.serialize(new_record).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
// Sleep to buy a bit of time for updated whitelist to be hot reloaded
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert!(whitelist
|
||||
.entries
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key("unit-test-api-key"));
|
||||
|
||||
// Delete the cloned whitelist
|
||||
std::fs::remove_file(&whitelist_csv_path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use structopt::StructOpt;
|
||||
|
||||
// Fields loaded from the command line when launching this server.
|
||||
#[derive(Clone, Debug, StructOpt)]
|
||||
#[structopt(name = "Notary Server")]
|
||||
pub struct CliFields {
|
||||
/// Configuration file location (optional).
|
||||
#[structopt(long)]
|
||||
pub config: Option<String>,
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
use config::{Config, Environment};
|
||||
use eyre::{eyre, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{parse_config_file, util::prepend_file_path, CliFields};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct NotaryServerProperties {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
/// Static html response returned from API root endpoint "/". Default html
|
||||
/// response contains placeholder strings that will be replaced with
|
||||
/// actual values in server.rs, e.g. {version}, {public_key}
|
||||
pub html_info: String,
|
||||
/// The maximum number of concurrent notarization sessions
|
||||
pub concurrency: usize,
|
||||
/// Setting for notarization
|
||||
pub notarization: NotarizationProperties,
|
||||
/// Setting for TLS connection between prover and notary
|
||||
pub tls: TLSProperties,
|
||||
/// Setting for logging
|
||||
pub log: LogProperties,
|
||||
/// Setting for authorization
|
||||
pub auth: AuthorizationProperties,
|
||||
}
|
||||
|
||||
impl NotaryServerProperties {
|
||||
pub fn new(cli_fields: &CliFields) -> Result<Self> {
|
||||
// Uses config file if given.
|
||||
if let Some(config_path) = &cli_fields.config {
|
||||
let mut config: NotaryServerProperties = parse_config_file(config_path)?;
|
||||
|
||||
// Ensures all relative file paths in the config file are prepended with
|
||||
// the config file's parent directory, so that server binary can be run from
|
||||
// anywhere.
|
||||
let parent_dir = Path::new(config_path)
|
||||
.parent()
|
||||
.ok_or(eyre!("Failed to get parent directory of config file"))?
|
||||
.to_str()
|
||||
.ok_or_else(|| eyre!("Failed to convert path to str"))?
|
||||
.to_string();
|
||||
|
||||
// Prepend notarization key path.
|
||||
if let Some(path) = config.notarization.private_key_path {
|
||||
config.notarization.private_key_path = Some(prepend_file_path(&path, &parent_dir)?);
|
||||
}
|
||||
// Prepend TLS key paths.
|
||||
if let Some(path) = config.tls.private_key_path {
|
||||
config.tls.private_key_path = Some(prepend_file_path(&path, &parent_dir)?);
|
||||
}
|
||||
if let Some(path) = config.tls.certificate_path {
|
||||
config.tls.certificate_path = Some(prepend_file_path(&path, &parent_dir)?);
|
||||
}
|
||||
// Prepend auth file path.
|
||||
if let Some(mode) = config.auth.mode {
|
||||
config.auth.mode = Some(match mode {
|
||||
AuthorizationModeProperties::Jwt(JwtAuthorizationProperties {
|
||||
algorithm,
|
||||
public_key_path,
|
||||
claims,
|
||||
}) => AuthorizationModeProperties::Jwt(JwtAuthorizationProperties {
|
||||
algorithm,
|
||||
public_key_path: prepend_file_path(&public_key_path, &parent_dir)?,
|
||||
claims,
|
||||
}),
|
||||
AuthorizationModeProperties::Whitelist(path) => {
|
||||
AuthorizationModeProperties::Whitelist(prepend_file_path(
|
||||
&path,
|
||||
&parent_dir,
|
||||
)?)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
} else {
|
||||
let default_config = Config::try_from(&NotaryServerProperties::default())?;
|
||||
|
||||
let config = Config::builder()
|
||||
.add_source(default_config)
|
||||
// Add in settings from environment variables (with a prefix of NS and '_' as
|
||||
// separator).
|
||||
.add_source(
|
||||
Environment::with_prefix("NS")
|
||||
.try_parsing(true)
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?
|
||||
.try_deserialize()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct NotarizationProperties {
|
||||
/// Global limit for maximum number of bytes that can be sent
|
||||
pub max_sent_data: usize,
|
||||
/// Global limit for maximum number of bytes that can be received
|
||||
pub max_recv_data: usize,
|
||||
/// Number of seconds before notarization timeouts to prevent unreleased
|
||||
/// memory
|
||||
pub timeout: u64,
|
||||
/// File path of private key (in PEM format) used to sign the notarization
|
||||
pub private_key_path: Option<String>,
|
||||
/// Signature algorithm used to generate a random private key when
|
||||
/// private_key_path is not set
|
||||
pub signature_algorithm: String,
|
||||
/// Flag to allow any custom extensions from the prover.
|
||||
pub allow_extensions: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct TLSProperties {
|
||||
/// Flag to turn on/off TLS between prover and notary — should always be
|
||||
/// turned on unless either
|
||||
/// (1) TLS is handled by external setup e.g. reverse proxy, cloud; or
|
||||
/// (2) For local testing
|
||||
pub enabled: bool,
|
||||
/// File path of TLS private key (in PEM format)
|
||||
pub private_key_path: Option<String>,
|
||||
/// File path of TLS cert (in PEM format)
|
||||
pub certificate_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum LogFormat {
|
||||
Compact,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct LogProperties {
|
||||
/// Log verbosity level of the default filtering logic, which is
|
||||
/// notary_server=<level>,tlsn_verifier=<level>,mpc_tls=<level>
|
||||
/// Must be either of <https://docs.rs/tracing/latest/tracing/struct.Level.html#implementations>
|
||||
pub level: String,
|
||||
/// Custom filtering logic, refer to the syntax here https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax
|
||||
/// This will override the default filtering logic above
|
||||
pub filter: Option<String>,
|
||||
/// Log format. Available options are "COMPACT" and "JSON"
|
||||
pub format: LogFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct AuthorizationProperties {
|
||||
/// Flag to turn on or off auth middleware
|
||||
pub enabled: bool,
|
||||
/// Authorization mode to use: JWT or Whitelist
|
||||
#[serde(flatten)]
|
||||
pub mode: Option<AuthorizationModeProperties>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum AuthorizationModeProperties {
|
||||
/// JWT authorization properties
|
||||
Jwt(JwtAuthorizationProperties),
|
||||
/// File path of the API key whitelist (in CSV format)
|
||||
Whitelist(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct JwtAuthorizationProperties {
|
||||
/// Algorithm used for signing the JWT
|
||||
pub algorithm: String,
|
||||
/// File path to JWT public key (in PEM format) for verifying token
|
||||
/// signatures
|
||||
pub public_key_path: String,
|
||||
/// Optional set of required JWT claims
|
||||
#[serde(default)]
|
||||
pub claims: Vec<JwtClaim>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct JwtClaim {
|
||||
/// Name of the claim
|
||||
pub name: String,
|
||||
/// Optional set of expected values for the claim
|
||||
#[serde(default)]
|
||||
pub values: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for NotaryServerProperties {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "0.0.0.0".to_string(),
|
||||
port: 7047,
|
||||
html_info: r#"
|
||||
<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>
|
||||
"#.to_string(),
|
||||
concurrency: 32,
|
||||
notarization: Default::default(),
|
||||
tls: Default::default(),
|
||||
log: Default::default(),
|
||||
auth: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NotarizationProperties {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_sent_data: 4096,
|
||||
max_recv_data: 16384,
|
||||
timeout: 1800,
|
||||
private_key_path: None,
|
||||
signature_algorithm: "secp256k1".to_string(),
|
||||
allow_extensions: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LogProperties {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: "DEBUG".to_string(),
|
||||
filter: None,
|
||||
format: LogFormat::Compact,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use axum::http::StatusCode;
|
||||
use axum_core::response::{IntoResponse as AxumCoreIntoResponse, Response};
|
||||
use eyre::Report;
|
||||
use std::error::Error;
|
||||
use tlsn::{
|
||||
config::ProtocolConfigValidatorBuilderError,
|
||||
verifier::{VerifierConfigBuilderError, VerifierError},
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NotaryServerError {
|
||||
#[error(transparent)]
|
||||
Unexpected(#[from] Report),
|
||||
#[error("Failed to connect to prover: {0}")]
|
||||
Connection(String),
|
||||
#[error("Error occurred during notarization: {0}")]
|
||||
Notarization(Box<dyn Error + Send + 'static>),
|
||||
#[error("Invalid request from prover: {0}")]
|
||||
BadProverRequest(String),
|
||||
#[error("Unauthorized request from prover: {0}")]
|
||||
UnauthorizedProverRequest(String),
|
||||
}
|
||||
|
||||
impl From<VerifierError> for NotaryServerError {
|
||||
fn from(error: VerifierError) -> Self {
|
||||
Self::Notarization(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VerifierConfigBuilderError> for NotaryServerError {
|
||||
fn from(error: VerifierConfigBuilderError) -> Self {
|
||||
Self::Notarization(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtocolConfigValidatorBuilderError> for NotaryServerError {
|
||||
fn from(error: ProtocolConfigValidatorBuilderError) -> Self {
|
||||
Self::Notarization(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implementation to convert this error into an axum http response
|
||||
impl AxumCoreIntoResponse for NotaryServerError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
bad_request_error @ NotaryServerError::BadProverRequest(_) => {
|
||||
(StatusCode::BAD_REQUEST, bad_request_error.to_string()).into_response()
|
||||
}
|
||||
unauthorized_request_error @ NotaryServerError::UnauthorizedProverRequest(_) => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
unauthorized_request_error.to_string(),
|
||||
)
|
||||
.into_response(),
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Something wrong happened.",
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
mod auth;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod error;
|
||||
mod middleware;
|
||||
mod server;
|
||||
mod server_tracing;
|
||||
mod service;
|
||||
mod signing;
|
||||
#[cfg(feature = "tee_quote")]
|
||||
mod tee;
|
||||
mod types;
|
||||
mod util;
|
||||
|
||||
pub use cli::CliFields;
|
||||
pub use config::{
|
||||
AuthorizationModeProperties, AuthorizationProperties, JwtAuthorizationProperties, JwtClaim,
|
||||
LogProperties, NotarizationProperties, NotaryServerProperties, TLSProperties,
|
||||
};
|
||||
pub use error::NotaryServerError;
|
||||
pub use server::{read_pem_file, run_server};
|
||||
pub use server_tracing::init_tracing;
|
||||
pub use util::parse_config_file;
|
||||
@@ -1,30 +0,0 @@
|
||||
use eyre::{eyre, Result};
|
||||
use notary_server::{
|
||||
init_tracing, run_server, CliFields, NotaryServerError, NotaryServerProperties,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
use tracing::debug;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), NotaryServerError> {
|
||||
// Load command line arguments
|
||||
let cli_fields: CliFields = CliFields::from_args();
|
||||
|
||||
let config = NotaryServerProperties::new(&cli_fields)
|
||||
.map_err(|err| eyre!("Failed to load config: {}", err))?;
|
||||
|
||||
// Set up tracing for logging
|
||||
init_tracing(&config).map_err(|err| eyre!("Failed to set up tracing: {err}"))?;
|
||||
|
||||
// debug!("Server config loaded: \n{}", config);
|
||||
|
||||
debug!(
|
||||
"Server config loaded: \n{}",
|
||||
serde_yaml::to_string(&config).map_err(|err| eyre!("Failed to print config: {err}"))?
|
||||
);
|
||||
|
||||
// Run the server
|
||||
run_server(&config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
use axum::http::{header, request::Parts};
|
||||
use axum_core::extract::{FromRef, FromRequestParts};
|
||||
use jsonwebtoken::{decode, TokenData, Validation};
|
||||
use notary_common::X_API_KEY_HEADER;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tracing::{error, trace};
|
||||
|
||||
use crate::{
|
||||
auth::{AuthorizationMode, AuthorizationWhitelistRecord},
|
||||
types::NotaryGlobals,
|
||||
NotaryServerError,
|
||||
};
|
||||
|
||||
/// Auth middleware to prevent DOS
|
||||
pub struct AuthorizationMiddleware;
|
||||
|
||||
impl<S> FromRequestParts<S> for AuthorizationMiddleware
|
||||
where
|
||||
NotaryGlobals: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = NotaryServerError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let notary_globals = NotaryGlobals::from_ref(state);
|
||||
let Some(mode) = notary_globals.authorization_mode else {
|
||||
trace!("Skipping authorization as it's not enabled.");
|
||||
return Ok(Self);
|
||||
};
|
||||
|
||||
match mode {
|
||||
AuthorizationMode::Whitelist(whitelist) => {
|
||||
let Some(auth_header) = parts
|
||||
.headers
|
||||
.get(X_API_KEY_HEADER)
|
||||
.and_then(|value| std::str::from_utf8(value.as_bytes()).ok())
|
||||
else {
|
||||
return Err(unauthorized("Missing API key"));
|
||||
};
|
||||
let entries = whitelist.entries.lock().unwrap();
|
||||
if api_key_is_valid(auth_header, &entries) {
|
||||
trace!("Request authorized.");
|
||||
Ok(Self)
|
||||
} else {
|
||||
Err(unauthorized("Invalid API key"))
|
||||
}
|
||||
}
|
||||
AuthorizationMode::Jwt(jwt_config) => {
|
||||
let Some(auth_header) = parts
|
||||
.headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| std::str::from_utf8(value.as_bytes()).ok())
|
||||
else {
|
||||
return Err(unauthorized("Missing JWT token"));
|
||||
};
|
||||
let raw_token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
|
||||
unauthorized("Invalid Authorization header: expected 'Bearer <token>'")
|
||||
})?;
|
||||
let validation = Validation::new(jwt_config.algorithm.into());
|
||||
let claims = match decode::<Value>(raw_token, &jwt_config.key, &validation) {
|
||||
Ok(TokenData { claims, .. }) => claims,
|
||||
Err(err) => {
|
||||
error!("Decoding JWT failed with error: {err:?}");
|
||||
return Err(unauthorized("Invalid JWT token"));
|
||||
}
|
||||
};
|
||||
if let Err(err) = jwt_config.validate(&claims) {
|
||||
error!("Validating JWT failed with error: {err:?}");
|
||||
return Err(unauthorized("Invalid JWT token"));
|
||||
};
|
||||
trace!("Request authorized.");
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unauthorized(err_msg: impl ToString) -> NotaryServerError {
|
||||
let err_msg = err_msg.to_string();
|
||||
error!(err_msg);
|
||||
NotaryServerError::UnauthorizedProverRequest(err_msg)
|
||||
}
|
||||
|
||||
/// Helper function to check if an API key is in whitelist
|
||||
fn api_key_is_valid(
|
||||
api_key: &str,
|
||||
whitelist: &HashMap<String, AuthorizationWhitelistRecord>,
|
||||
) -> bool {
|
||||
whitelist.get(api_key).is_some()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{api_key_is_valid, HashMap};
|
||||
use crate::auth::{
|
||||
whitelist::authorization_whitelist_vec_into_hashmap, AuthorizationWhitelistRecord,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn get_whitelist_fixture() -> HashMap<String, AuthorizationWhitelistRecord> {
|
||||
authorization_whitelist_vec_into_hashmap(vec![
|
||||
AuthorizationWhitelistRecord {
|
||||
name: "test-name-0".to_string(),
|
||||
api_key: "test-api-key-0".to_string(),
|
||||
created_at: "2023-10-18T07:38:53Z".to_string(),
|
||||
},
|
||||
AuthorizationWhitelistRecord {
|
||||
name: "test-name-1".to_string(),
|
||||
api_key: "test-api-key-1".to_string(),
|
||||
created_at: "2023-10-11T07:38:53Z".to_string(),
|
||||
},
|
||||
AuthorizationWhitelistRecord {
|
||||
name: "test-name-2".to_string(),
|
||||
api_key: "test-api-key-2".to_string(),
|
||||
created_at: "2022-10-11T07:38:53Z".to_string(),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_key_is_present() {
|
||||
let whitelist = get_whitelist_fixture();
|
||||
assert!(api_key_is_valid("test-api-key-0", &Arc::new(whitelist)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_key_is_absent() {
|
||||
let whitelist = get_whitelist_fixture();
|
||||
assert!(!api_key_is_valid("test-api-keY-0", &Arc::new(whitelist)));
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
use axum::{
|
||||
extract::Request,
|
||||
http::StatusCode,
|
||||
middleware::from_extractor_with_state,
|
||||
response::{Html, IntoResponse},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use eyre::{ensure, eyre, Result};
|
||||
use futures_util::future::poll_fn;
|
||||
use hyper::{body::Incoming, server::conn::http1};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use pkcs8::DecodePrivateKey;
|
||||
use rustls::{Certificate, PrivateKey, ServerConfig};
|
||||
use std::{
|
||||
fs::File as StdFile,
|
||||
io::BufReader,
|
||||
net::{IpAddr, SocketAddr},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
use tlsn::attestation::CryptoProvider;
|
||||
use tokio::{fs::File, io::AsyncReadExt, net::TcpListener};
|
||||
use tokio_rustls::{rustls, TlsAcceptor};
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_service::Service;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
auth::{load_authorization_mode, watch_and_reload_authorization_whitelist, AuthorizationMode},
|
||||
config::{NotarizationProperties, NotaryServerProperties},
|
||||
error::NotaryServerError,
|
||||
middleware::AuthorizationMiddleware,
|
||||
service::{initialize, upgrade_protocol},
|
||||
signing::AttestationKey,
|
||||
types::{InfoResponse, NotaryGlobals},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tee_quote")]
|
||||
use crate::tee::quote;
|
||||
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
/// Start a TCP server (with or without TLS) to accept notarization request for
|
||||
/// both TCP and WebSocket clients
|
||||
#[tracing::instrument(skip(config))]
|
||||
pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotaryServerError> {
|
||||
let attestation_key = get_attestation_key(&config.notarization).await?;
|
||||
let verifying_key_pem = attestation_key
|
||||
.verifying_key_pem()
|
||||
.map_err(|err| eyre!("Failed to get verifying key in PEM format: {err}"))?;
|
||||
|
||||
#[cfg(feature = "tee_quote")]
|
||||
let verifying_key_bytes = attestation_key.verifying_key_bytes();
|
||||
|
||||
let crypto_provider = build_crypto_provider(attestation_key);
|
||||
|
||||
// Build TLS acceptor if it is turned on
|
||||
let tls_acceptor = if !config.tls.enabled {
|
||||
debug!("Skipping TLS setup as it is turned off.");
|
||||
None
|
||||
} else {
|
||||
let private_key_pem_path = config
|
||||
.tls
|
||||
.private_key_path
|
||||
.as_deref()
|
||||
.ok_or_else(|| eyre!("TLS is enabled but private key PEM path is not set"))?;
|
||||
let certificate_pem_path = config
|
||||
.tls
|
||||
.certificate_path
|
||||
.as_deref()
|
||||
.ok_or_else(|| eyre!("TLS is enabled but certificate PEM path is not set"))?;
|
||||
|
||||
let (tls_private_key, tls_certificates) =
|
||||
load_tls_key_and_cert(private_key_pem_path, certificate_pem_path).await?;
|
||||
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(tls_certificates, tls_private_key)
|
||||
.map_err(|err| eyre!("Failed to instantiate notary server tls config: {err}"))?;
|
||||
|
||||
// Set the http protocols we support
|
||||
server_config.alpn_protocols = vec![b"http/1.1".to_vec()];
|
||||
let tls_config = Arc::new(server_config);
|
||||
Some(TlsAcceptor::from(tls_config))
|
||||
};
|
||||
|
||||
// Set up authorization if it is turned on
|
||||
let authorization_mode = load_authorization_mode(config).await?;
|
||||
// Enable hot reload if authorization whitelist is available
|
||||
let watcher = authorization_mode
|
||||
.as_ref()
|
||||
.and_then(AuthorizationMode::as_whitelist)
|
||||
.map(watch_and_reload_authorization_whitelist)
|
||||
.transpose()?;
|
||||
if watcher.is_some() {
|
||||
debug!("Successfully setup watcher for hot reload of authorization whitelist!");
|
||||
}
|
||||
|
||||
let notary_address = SocketAddr::new(
|
||||
IpAddr::V4(config.host.parse().map_err(|err| {
|
||||
eyre!("Failed to parse notary host address from server config: {err}")
|
||||
})?),
|
||||
config.port,
|
||||
);
|
||||
let mut listener = TcpListener::bind(notary_address)
|
||||
.await
|
||||
.map_err(|err| eyre!("Failed to bind server address to tcp listener: {err}"))?;
|
||||
|
||||
info!("Listening for TCP traffic at {}", notary_address);
|
||||
|
||||
let protocol = Arc::new(http1::Builder::new());
|
||||
let notary_globals = NotaryGlobals::new(
|
||||
Arc::new(crypto_provider),
|
||||
config.notarization.clone(),
|
||||
authorization_mode,
|
||||
Arc::new(Semaphore::new(config.concurrency)),
|
||||
);
|
||||
|
||||
// Parameters needed for the info endpoint
|
||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||
let git_commit_hash = env!("GIT_COMMIT_HASH").to_string();
|
||||
|
||||
// Parameters needed for the root / endpoint
|
||||
let html_string = config.html_info.clone();
|
||||
let html_info = Html(
|
||||
html_string
|
||||
.replace("{version}", &version)
|
||||
.replace("{git_commit_hash}", &git_commit_hash)
|
||||
.replace("{public_key}", &verifying_key_pem),
|
||||
);
|
||||
|
||||
let router = Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(|| async move { (StatusCode::OK, html_info).into_response() }),
|
||||
)
|
||||
.route(
|
||||
"/healthcheck",
|
||||
get(|| async move { (StatusCode::OK, "Ok").into_response() }),
|
||||
)
|
||||
.route(
|
||||
"/info",
|
||||
get(|| async move {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(InfoResponse {
|
||||
version,
|
||||
public_key: verifying_key_pem,
|
||||
git_commit_hash,
|
||||
#[cfg(feature = "tee_quote")]
|
||||
quote: quote(verifying_key_bytes).await,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}),
|
||||
)
|
||||
.route("/session", post(initialize))
|
||||
// Not applying auth middleware to /notarize endpoint for now as we can rely on our
|
||||
// short-lived session id generated from /session endpoint, as it is not possible
|
||||
// to use header for API key for websocket /notarize endpoint due to browser restriction
|
||||
// ref: https://stackoverflow.com/a/4361358; And putting it in url query param
|
||||
// seems to be more insecured: https://stackoverflow.com/questions/5517281/place-api-key-in-headers-or-url
|
||||
.route_layer(from_extractor_with_state::<
|
||||
AuthorizationMiddleware,
|
||||
NotaryGlobals,
|
||||
>(notary_globals.clone()))
|
||||
.route("/notarize", get(upgrade_protocol))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(notary_globals);
|
||||
|
||||
loop {
|
||||
// Poll and await for any incoming connection, ensure that all operations inside
|
||||
// are infallible to prevent bringing down the server
|
||||
let stream = match poll_fn(|cx| Pin::new(&mut listener).poll_accept(cx)).await {
|
||||
Ok((stream, _)) => stream,
|
||||
Err(err) => {
|
||||
error!("{}", NotaryServerError::Connection(err.to_string()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Setting TCP_NODELAY will improve notary latency.
|
||||
let _ = stream.set_nodelay(true).map_err(|_| {
|
||||
info!("An error occured when setting TCP_NODELAY. This will result in higher protocol latency.");
|
||||
});
|
||||
|
||||
debug!("Received a prover's TCP connection");
|
||||
|
||||
let tower_service = router.clone();
|
||||
let tls_acceptor = tls_acceptor.clone();
|
||||
let protocol = protocol.clone();
|
||||
|
||||
// Spawn a new async task to handle the new connection
|
||||
tokio::spawn(async move {
|
||||
// When TLS is enabled
|
||||
if let Some(acceptor) = tls_acceptor {
|
||||
match acceptor.accept(stream).await {
|
||||
Ok(stream) => {
|
||||
info!("Accepted prover's TLS-secured TCP connection");
|
||||
// Reference: https://github.com/tokio-rs/axum/blob/5201798d4e4d4759c208ef83e30ce85820c07baa/examples/low-level-rustls/src/main.rs#L67-L80
|
||||
let io = TokioIo::new(stream);
|
||||
let hyper_service =
|
||||
hyper::service::service_fn(move |request: Request<Incoming>| {
|
||||
tower_service.clone().call(request)
|
||||
});
|
||||
// Serve different requests using the same hyper protocol and axum router
|
||||
let _ = protocol
|
||||
.serve_connection(io, hyper_service)
|
||||
// use with_upgrades to upgrade connection to websocket for websocket
|
||||
// clients and to extract tcp connection for
|
||||
// tcp clients
|
||||
.with_upgrades()
|
||||
.await;
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
error!("{}", NotaryServerError::Connection(err.to_string()));
|
||||
|
||||
if let Some(rustls::Error::InvalidMessage(
|
||||
rustls::InvalidMessage::InvalidContentType,
|
||||
)) = err
|
||||
.get_ref()
|
||||
.and_then(|inner| inner.downcast_ref::<rustls::Error>())
|
||||
{
|
||||
error!("Perhaps the client is connecting without TLS");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// When TLS is disabled
|
||||
info!("Accepted prover's TCP connection",);
|
||||
// Reference: https://github.com/tokio-rs/axum/blob/5201798d4e4d4759c208ef83e30ce85820c07baa/examples/low-level-rustls/src/main.rs#L67-L80
|
||||
let io = TokioIo::new(stream);
|
||||
let hyper_service =
|
||||
hyper::service::service_fn(move |request: Request<Incoming>| {
|
||||
tower_service.clone().call(request)
|
||||
});
|
||||
// Serve different requests using the same hyper protocol and axum router
|
||||
let _ = protocol
|
||||
.serve_connection(io, hyper_service)
|
||||
// use with_upgrades to upgrade connection to websocket for websocket clients
|
||||
// and to extract tcp connection for tcp clients
|
||||
.with_upgrades()
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn build_crypto_provider(attestation_key: AttestationKey) -> CryptoProvider {
|
||||
let mut provider = CryptoProvider::default();
|
||||
provider.signer.set_signer(attestation_key.into_signer());
|
||||
provider
|
||||
}
|
||||
|
||||
/// Get notary signing key for attestations.
|
||||
/// Generate a random key if user does not provide a static key.
|
||||
async fn get_attestation_key(config: &NotarizationProperties) -> Result<AttestationKey> {
|
||||
let key = if let Some(private_key_path) = &config.private_key_path {
|
||||
debug!("Loading notary server's signing key");
|
||||
|
||||
let mut file = File::open(private_key_path).await?;
|
||||
let mut pem = String::new();
|
||||
file.read_to_string(&mut pem)
|
||||
.await
|
||||
.map_err(|_| eyre!("pem file does not contain valid UTF-8"))?;
|
||||
|
||||
let key = AttestationKey::from_pkcs8_pem(&pem)
|
||||
.map_err(|err| eyre!("Failed to load notary signing key for notarization: {err}"))?;
|
||||
|
||||
pem.zeroize();
|
||||
|
||||
key
|
||||
} else {
|
||||
warn!(
|
||||
"⚠️ Using a random, ephemeral signing key because `notarization.private_key_path` is not set."
|
||||
);
|
||||
AttestationKey::random(&config.signature_algorithm)?
|
||||
};
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Read a PEM-formatted file and return its buffer reader
|
||||
pub async fn read_pem_file(file_path: &str) -> Result<BufReader<StdFile>> {
|
||||
let key_file = File::open(file_path).await?.into_std().await;
|
||||
Ok(BufReader::new(key_file))
|
||||
}
|
||||
|
||||
/// Load notary tls private key and cert from static files
|
||||
async fn load_tls_key_and_cert(
|
||||
private_key_pem_path: &str,
|
||||
certificate_pem_path: &str,
|
||||
) -> Result<(PrivateKey, Vec<Certificate>)> {
|
||||
debug!("Loading notary server's tls private key and certificate");
|
||||
|
||||
let mut private_key_file_reader = read_pem_file(private_key_pem_path).await?;
|
||||
let mut private_keys = rustls_pemfile::pkcs8_private_keys(&mut private_key_file_reader)?;
|
||||
ensure!(
|
||||
private_keys.len() == 1,
|
||||
"More than 1 key found in the tls private key pem file"
|
||||
);
|
||||
let private_key = PrivateKey(private_keys.remove(0));
|
||||
|
||||
let mut certificate_file_reader = read_pem_file(certificate_pem_path).await?;
|
||||
let certificates = rustls_pemfile::certs(&mut certificate_file_reader)?
|
||||
.into_iter()
|
||||
.map(Certificate)
|
||||
.collect();
|
||||
|
||||
debug!("Successfully loaded notary server's tls private key and certificate!");
|
||||
Ok((private_key, certificates))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_tls_key_and_cert() {
|
||||
let private_key_pem_path = "../tests-integration/fixture/tls/notary.key";
|
||||
let certificate_pem_path = "../tests-integration/fixture/tls/notary.crt";
|
||||
let result: Result<(PrivateKey, Vec<Certificate>)> =
|
||||
load_tls_key_and_cert(private_key_pem_path, certificate_pem_path).await;
|
||||
assert!(result.is_ok(), "Could not load tls private key and cert");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_attestation_key() {
|
||||
let config = NotarizationProperties {
|
||||
private_key_path: Some("../tests-integration/fixture/notary/notary.key".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let result = get_attestation_key(&config).await;
|
||||
assert!(result.is_ok(), "Could not load attestation key");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_attestation_key() {
|
||||
let config = NotarizationProperties {
|
||||
private_key_path: None,
|
||||
..Default::default()
|
||||
};
|
||||
let result = get_attestation_key(&config).await;
|
||||
assert!(result.is_ok(), "Could not generate attestation key");
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use eyre::Result;
|
||||
use std::str::FromStr;
|
||||
use tracing::{Level, Subscriber};
|
||||
use tracing_subscriber::{
|
||||
fmt, layer::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt, EnvFilter, Layer,
|
||||
Registry,
|
||||
};
|
||||
|
||||
use crate::config::{LogFormat, NotaryServerProperties};
|
||||
|
||||
fn format_layer<S>(format: LogFormat) -> Box<dyn Layer<S> + Send + Sync>
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
{
|
||||
let f = fmt::layer().with_thread_ids(true).with_thread_names(true);
|
||||
match format {
|
||||
LogFormat::Compact => f.compact().boxed(),
|
||||
LogFormat::Json => f.json().boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_tracing(config: &NotaryServerProperties) -> Result<()> {
|
||||
// Retrieve log filtering logic from config
|
||||
let directives = match &config.log.filter {
|
||||
// Use custom filter that is provided by user
|
||||
Some(filter) => filter.clone(),
|
||||
// Use the default filter when only verbosity level is provided
|
||||
None => {
|
||||
let level = Level::from_str(&config.log.level)?;
|
||||
format!("notary_server={level},tlsn_verifier={level},mpc_tls={level}")
|
||||
}
|
||||
};
|
||||
let filter_layer = EnvFilter::builder().parse(directives)?;
|
||||
|
||||
Registry::default()
|
||||
.with(filter_layer)
|
||||
.with(format_layer(config.log.format))
|
||||
.try_init()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
pub mod axum_websocket;
|
||||
pub mod tcp;
|
||||
pub mod websocket;
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{rejection::JsonRejection, FromRequestParts, Query, State},
|
||||
http::{header, request::Parts, StatusCode},
|
||||
response::{IntoResponse, Json, Response},
|
||||
};
|
||||
use axum_macros::debug_handler;
|
||||
use eyre::eyre;
|
||||
use notary_common::{NotarizationSessionRequest, NotarizationSessionResponse};
|
||||
use std::time::Duration;
|
||||
use tlsn::{
|
||||
attestation::AttestationConfig,
|
||||
config::ProtocolConfigValidator,
|
||||
verifier::{Verifier, VerifierConfig},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
time::timeout,
|
||||
};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use tracing::{debug, error, info, trace};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
error::NotaryServerError,
|
||||
service::{
|
||||
axum_websocket::{header_eq, WebSocketUpgrade},
|
||||
tcp::{tcp_notarize, TcpUpgrade},
|
||||
websocket::websocket_notarize,
|
||||
},
|
||||
types::{NotarizationRequestQuery, NotaryGlobals},
|
||||
};
|
||||
|
||||
/// A wrapper enum to facilitate extracting TCP connection for either WebSocket
|
||||
/// or TCP clients, so that we can use a single endpoint and handler for
|
||||
/// notarization for both types of clients
|
||||
pub enum ProtocolUpgrade {
|
||||
Tcp(TcpUpgrade),
|
||||
Ws(WebSocketUpgrade),
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for ProtocolUpgrade
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = NotaryServerError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
// Extract tcp connection for websocket client
|
||||
if header_eq(&parts.headers, header::UPGRADE, "websocket") {
|
||||
let extractor = WebSocketUpgrade::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|err| NotaryServerError::BadProverRequest(err.to_string()))?;
|
||||
Ok(Self::Ws(extractor))
|
||||
// Extract tcp connection for tcp client
|
||||
} else if header_eq(&parts.headers, header::UPGRADE, "tcp") {
|
||||
let extractor = TcpUpgrade::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|err| NotaryServerError::BadProverRequest(err.to_string()))?;
|
||||
Ok(Self::Tcp(extractor))
|
||||
} else {
|
||||
Err(NotaryServerError::BadProverRequest(
|
||||
"Upgrade header is not set for client".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler to upgrade protocol from http to either websocket or underlying tcp
|
||||
/// depending on the type of client the session_id parameter is also extracted
|
||||
/// here to fetch the configuration parameters that have been submitted in the
|
||||
/// previous request to /session made by the same client
|
||||
pub async fn upgrade_protocol(
|
||||
protocol_upgrade: ProtocolUpgrade,
|
||||
State(notary_globals): State<NotaryGlobals>,
|
||||
Query(params): Query<NotarizationRequestQuery>,
|
||||
) -> Response {
|
||||
let permit = if let Ok(permit) = notary_globals.semaphore.clone().try_acquire_owned() {
|
||||
permit
|
||||
} else {
|
||||
// TODO: estimate the time more precisely to avoid unnecessary retries.
|
||||
return Response::builder()
|
||||
.status(StatusCode::SERVICE_UNAVAILABLE)
|
||||
.header("Retry-After", 5)
|
||||
.body(Body::default())
|
||||
.expect("Builder should not fail");
|
||||
};
|
||||
|
||||
info!("Received upgrade protocol request");
|
||||
let session_id = params.session_id;
|
||||
// Check if session_id exists in the store, this also removes session_id from
|
||||
// the store as each session_id can only be used once
|
||||
if notary_globals
|
||||
.store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&session_id)
|
||||
.is_none()
|
||||
{
|
||||
let err_msg = format!("Session id {session_id} does not exist");
|
||||
error!(err_msg);
|
||||
return NotaryServerError::BadProverRequest(err_msg).into_response();
|
||||
};
|
||||
// This completes the HTTP Upgrade request and returns a successful response to
|
||||
// the client, meanwhile initiating the websocket or tcp connection
|
||||
match protocol_upgrade {
|
||||
ProtocolUpgrade::Ws(ws) => ws.on_upgrade(move |socket| async move {
|
||||
websocket_notarize(socket, notary_globals, session_id).await;
|
||||
drop(permit);
|
||||
}),
|
||||
ProtocolUpgrade::Tcp(tcp) => tcp.on_upgrade(move |stream| async move {
|
||||
tcp_notarize(stream, notary_globals, session_id).await;
|
||||
drop(permit);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler to initialize and configure notarization for both TCP and WebSocket
|
||||
/// clients
|
||||
#[debug_handler(state = NotaryGlobals)]
|
||||
pub async fn initialize(
|
||||
State(notary_globals): State<NotaryGlobals>,
|
||||
payload: Result<Json<NotarizationSessionRequest>, JsonRejection>,
|
||||
) -> impl IntoResponse {
|
||||
info!(
|
||||
?payload,
|
||||
"Received request for initializing a notarization session"
|
||||
);
|
||||
|
||||
// Parse the body payload
|
||||
let payload = match payload {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => {
|
||||
error!("Malformed payload submitted for initializing notarization: {err}");
|
||||
return NotaryServerError::BadProverRequest(err.to_string()).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure that the max_sent_data, max_recv_data submitted is not larger than the
|
||||
// global max limits configured in notary server
|
||||
if payload.max_sent_data.is_some() || payload.max_recv_data.is_some() {
|
||||
if payload.max_sent_data.unwrap_or_default()
|
||||
> notary_globals.notarization_config.max_sent_data
|
||||
{
|
||||
error!(
|
||||
"Max sent data requested {:?} exceeds the global maximum threshold {:?}",
|
||||
payload.max_sent_data.unwrap_or_default(),
|
||||
notary_globals.notarization_config.max_sent_data
|
||||
);
|
||||
return NotaryServerError::BadProverRequest(
|
||||
"Max sent data requested exceeds the global maximum threshold".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if payload.max_recv_data.unwrap_or_default()
|
||||
> notary_globals.notarization_config.max_recv_data
|
||||
{
|
||||
error!(
|
||||
"Max recv data requested {:?} exceeds the global maximum threshold {:?}",
|
||||
payload.max_recv_data.unwrap_or_default(),
|
||||
notary_globals.notarization_config.max_recv_data
|
||||
);
|
||||
return NotaryServerError::BadProverRequest(
|
||||
"Max recv data requested exceeds the global maximum threshold".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let prover_session_id = Uuid::new_v4().to_string();
|
||||
|
||||
// Store the configuration data in a temporary store
|
||||
notary_globals
|
||||
.store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(prover_session_id.clone(), ());
|
||||
|
||||
trace!("Latest store state: {:?}", notary_globals.store);
|
||||
|
||||
// Return the session id in the response to the client
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(NotarizationSessionResponse {
|
||||
session_id: prover_session_id,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Run the notarization
|
||||
pub async fn notary_service<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
socket: T,
|
||||
notary_globals: NotaryGlobals,
|
||||
session_id: &str,
|
||||
) -> Result<(), NotaryServerError> {
|
||||
debug!(?session_id, "Starting notarization...");
|
||||
|
||||
let crypto_provider = notary_globals.crypto_provider.clone();
|
||||
|
||||
let mut att_config_builder = AttestationConfig::builder();
|
||||
att_config_builder
|
||||
.supported_signature_algs(Vec::from_iter(crypto_provider.signer.supported_algs()));
|
||||
|
||||
// If enabled, accepts any custom extensions from the prover.
|
||||
if notary_globals.notarization_config.allow_extensions {
|
||||
att_config_builder.extension_validator(|_| Ok(()));
|
||||
}
|
||||
|
||||
let att_config = att_config_builder
|
||||
.build()
|
||||
.map_err(|err| NotaryServerError::Notarization(Box::new(err)))?;
|
||||
|
||||
let config = VerifierConfig::builder()
|
||||
.protocol_config_validator(
|
||||
ProtocolConfigValidator::builder()
|
||||
.max_sent_data(notary_globals.notarization_config.max_sent_data)
|
||||
.max_recv_data(notary_globals.notarization_config.max_recv_data)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
timeout(
|
||||
Duration::from_secs(notary_globals.notarization_config.timeout),
|
||||
Verifier::new(config).notarize_with_provider(
|
||||
socket.compat(),
|
||||
&att_config,
|
||||
&crypto_provider,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| eyre!("Timeout reached before notarization completes"))??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,106 +0,0 @@
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::{header, request::Parts, HeaderValue, StatusCode},
|
||||
response::Response,
|
||||
};
|
||||
use axum_core::body::Body;
|
||||
use hyper::upgrade::{OnUpgrade, Upgraded};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use std::future::Future;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::{service::notary_service, types::NotaryGlobals, NotaryServerError};
|
||||
|
||||
/// Custom extractor used to extract underlying TCP connection for TCP client —
|
||||
/// using the same upgrade primitives used by the WebSocket implementation where
|
||||
/// the underlying TCP connection (wrapped in an Upgraded object) only gets
|
||||
/// polled as an OnUpgrade future after the ongoing HTTP request is finished (ref: https://github.com/tokio-rs/axum/blob/a6a849bb5b96a2f641fa077fe76f70ad4d20341c/axum/src/extract/ws.rs#L122)
|
||||
///
|
||||
/// More info on the upgrade primitives: https://docs.rs/hyper/latest/hyper/upgrade/index.html
|
||||
pub struct TcpUpgrade {
|
||||
pub on_upgrade: OnUpgrade,
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for TcpUpgrade
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = NotaryServerError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let on_upgrade =
|
||||
parts
|
||||
.extensions
|
||||
.remove::<OnUpgrade>()
|
||||
.ok_or(NotaryServerError::BadProverRequest(
|
||||
"Upgrade header is not set for TCP client".to_string(),
|
||||
))?;
|
||||
|
||||
Ok(Self { on_upgrade })
|
||||
}
|
||||
}
|
||||
|
||||
impl TcpUpgrade {
|
||||
/// Utility function to complete the http upgrade protocol by
|
||||
/// (1) Return 101 switching protocol response to client to indicate the
|
||||
/// switching to TCP (2) Spawn a new thread to await on the OnUpgrade
|
||||
/// object to claim the underlying TCP connection
|
||||
pub fn on_upgrade<C, Fut>(self, callback: C) -> Response
|
||||
where
|
||||
C: FnOnce(TokioIo<Upgraded>) -> Fut + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let on_upgrade = self.on_upgrade;
|
||||
tokio::spawn(async move {
|
||||
let upgraded = match on_upgrade.await {
|
||||
Ok(upgraded) => upgraded,
|
||||
Err(err) => {
|
||||
error!("Something wrong with upgrading HTTP: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let upgraded = TokioIo::new(upgraded);
|
||||
|
||||
callback(upgraded).await;
|
||||
});
|
||||
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const UPGRADE: HeaderValue = HeaderValue::from_static("upgrade");
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const TCP: HeaderValue = HeaderValue::from_static("tcp");
|
||||
|
||||
let builder = Response::builder()
|
||||
.status(StatusCode::SWITCHING_PROTOCOLS)
|
||||
.header(header::CONNECTION, UPGRADE)
|
||||
.header(header::UPGRADE, TCP);
|
||||
|
||||
builder.body(Body::empty()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform notarization using the extracted tcp connection
|
||||
pub async fn tcp_notarize(
|
||||
stream: TokioIo<Upgraded>,
|
||||
notary_globals: NotaryGlobals,
|
||||
session_id: String,
|
||||
) {
|
||||
let start = Instant::now();
|
||||
debug!(?session_id, "Upgraded to tcp connection");
|
||||
match notary_service(stream, notary_globals, &session_id).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
?session_id,
|
||||
elapsed_time_millis = start.elapsed().as_millis(),
|
||||
"Successful notarization using tcp!"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
?session_id,
|
||||
elapsed_time_millis = start.elapsed().as_millis(),
|
||||
"Failed notarization using tcp: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, info};
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
use crate::{
|
||||
service::{axum_websocket::WebSocket, notary_service},
|
||||
types::NotaryGlobals,
|
||||
};
|
||||
|
||||
/// Perform notarization using the established websocket connection
|
||||
pub async fn websocket_notarize(
|
||||
socket: WebSocket,
|
||||
notary_globals: NotaryGlobals,
|
||||
session_id: String,
|
||||
) {
|
||||
let start = Instant::now();
|
||||
debug!(?session_id, "Upgraded to websocket connection");
|
||||
// Wrap the websocket in WsStream so that we have AsyncRead and AsyncWrite
|
||||
// implemented
|
||||
let stream = WsStream::new(socket.into_inner());
|
||||
match notary_service(stream, notary_globals, &session_id).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
?session_id,
|
||||
elapsed_time_millis = start.elapsed().as_millis(),
|
||||
"Successful notarization using websocket!"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
?session_id,
|
||||
elapsed_time_millis = start.elapsed().as_millis(),
|
||||
"Failed notarization using websocket: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
use const_oid::db::rfc5912::ID_EC_PUBLIC_KEY as OID_EC_PUBLIC_KEY;
|
||||
use core::fmt;
|
||||
use eyre::{eyre, Result};
|
||||
use pkcs8::{
|
||||
der::{self, pem::PemLabel, Encode},
|
||||
spki::{DynAssociatedAlgorithmIdentifier, SubjectPublicKeyInfoRef},
|
||||
AssociatedOid, DecodePrivateKey, LineEnding, PrivateKeyInfo,
|
||||
};
|
||||
use rand06_compat::Rand0_6CompatExt;
|
||||
use tlsn::attestation::signing::{Secp256k1Signer, Secp256r1Signer, SignatureAlgId, Signer};
|
||||
use tracing::error;
|
||||
|
||||
/// A cryptographic key used for signing attestations.
|
||||
pub struct AttestationKey {
|
||||
alg_id: SignatureAlgId,
|
||||
key: SigningKey,
|
||||
}
|
||||
|
||||
impl TryFrom<PrivateKeyInfo<'_>> for AttestationKey {
|
||||
type Error = pkcs8::Error;
|
||||
|
||||
fn try_from(pkcs8: PrivateKeyInfo<'_>) -> Result<Self, Self::Error> {
|
||||
// For now we only support elliptic curve keys.
|
||||
if pkcs8.algorithm.oid != OID_EC_PUBLIC_KEY {
|
||||
error!("unsupported key algorithm OID: {:?}", pkcs8.algorithm.oid);
|
||||
|
||||
return Err(pkcs8::Error::KeyMalformed);
|
||||
}
|
||||
|
||||
let (alg_id, key) = match pkcs8.algorithm.parameters_oid()? {
|
||||
k256::Secp256k1::OID => {
|
||||
let key = k256::ecdsa::SigningKey::from_pkcs8_der(&pkcs8.to_der()?)
|
||||
.map_err(|_| pkcs8::Error::KeyMalformed)?;
|
||||
(SignatureAlgId::SECP256K1, SigningKey::Secp256k1(key))
|
||||
}
|
||||
p256::NistP256::OID => {
|
||||
let key = p256::ecdsa::SigningKey::from_pkcs8_der(&pkcs8.to_der()?)
|
||||
.map_err(|_| pkcs8::Error::KeyMalformed)?;
|
||||
(SignatureAlgId::SECP256R1, SigningKey::Secp256r1(key))
|
||||
}
|
||||
oid => {
|
||||
error!("unsupported curve OID: {:?}", oid);
|
||||
|
||||
return Err(pkcs8::Error::KeyMalformed);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self { alg_id, key })
|
||||
}
|
||||
}
|
||||
|
||||
impl AttestationKey {
|
||||
/// Samples a new attestation key of the given signature algorithm.
|
||||
pub fn random(alg_id: &str) -> Result<Self> {
|
||||
match alg_id.to_uppercase().as_str() {
|
||||
"SECP256K1" => Ok(Self {
|
||||
alg_id: SignatureAlgId::SECP256K1,
|
||||
key: SigningKey::Secp256k1(k256::ecdsa::SigningKey::random(
|
||||
&mut rand::rng().compat(),
|
||||
)),
|
||||
}),
|
||||
"SECP256R1" => Ok(Self {
|
||||
alg_id: SignatureAlgId::SECP256R1,
|
||||
key: SigningKey::Secp256r1(p256::ecdsa::SigningKey::random(
|
||||
&mut rand::rng().compat(),
|
||||
)),
|
||||
}),
|
||||
alg_id => Err(eyre!("unsupported signature algorithm: {alg_id} — only secp256k1 and secp256r1 are supported")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new signer using this key.
|
||||
pub fn into_signer(self) -> Box<dyn Signer + Send + Sync> {
|
||||
match self.key {
|
||||
SigningKey::Secp256k1(key) => {
|
||||
Box::new(Secp256k1Signer::new(&key.to_bytes()).expect("key should be valid"))
|
||||
}
|
||||
SigningKey::Secp256r1(key) => {
|
||||
Box::new(Secp256r1Signer::new(&key.to_bytes()).expect("key should be valid"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the verifying key in compressed bytes.
|
||||
pub fn verifying_key_bytes(&self) -> Vec<u8> {
|
||||
match self.key {
|
||||
SigningKey::Secp256k1(ref key) => key
|
||||
.verifying_key()
|
||||
.to_encoded_point(true)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
SigningKey::Secp256r1(ref key) => key
|
||||
.verifying_key()
|
||||
.to_encoded_point(true)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the verifying key in compressed PEM format.
|
||||
pub fn verifying_key_pem(&self) -> Result<String, pkcs8::spki::Error> {
|
||||
let algorithm = match &self.key {
|
||||
SigningKey::Secp256k1(key) => key.verifying_key().algorithm_identifier()?,
|
||||
SigningKey::Secp256r1(key) => key.verifying_key().algorithm_identifier()?,
|
||||
};
|
||||
let verifying_key_bytes = self.verifying_key_bytes();
|
||||
let subject_public_key = der::asn1::BitStringRef::new(0, &verifying_key_bytes)?;
|
||||
|
||||
let der: der::Document = pkcs8::SubjectPublicKeyInfo {
|
||||
algorithm,
|
||||
subject_public_key,
|
||||
}
|
||||
.try_into()?;
|
||||
|
||||
let pem = der.to_pem(SubjectPublicKeyInfoRef::PEM_LABEL, LineEnding::LF)?;
|
||||
|
||||
Ok(pem)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for AttestationKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AttestationKey")
|
||||
.field("alg_id", &self.alg_id)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
enum SigningKey {
|
||||
Secp256k1(k256::ecdsa::SigningKey),
|
||||
Secp256r1(p256::ecdsa::SigningKey),
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[test]
|
||||
fn test_verifying_key_pem() {
|
||||
let attestation_key_pem =
|
||||
read_to_string("../tests-integration/fixture/notary/notary.key").unwrap();
|
||||
|
||||
let attestation_key = AttestationKey::from_pkcs8_pem(&attestation_key_pem).unwrap();
|
||||
|
||||
let verifying_key_pem = attestation_key.verifying_key_pem().unwrap();
|
||||
|
||||
let expected_verifying_key_pem =
|
||||
read_to_string("../tests-integration/fixture/notary/notary.pub").unwrap();
|
||||
|
||||
assert_eq!(verifying_key_pem, expected_verifying_key_pem);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
use mc_sgx_dcap_types::{QlError, Quote3};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs,
|
||||
fs::File,
|
||||
io::{self, Read},
|
||||
path::Path,
|
||||
};
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Quote {
|
||||
raw_quote: Option<String>,
|
||||
mrsigner: Option<String>,
|
||||
mrenclave: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Quote {
|
||||
fn default() -> Quote {
|
||||
Quote {
|
||||
raw_quote: Some("".to_string()),
|
||||
mrsigner: None,
|
||||
mrenclave: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for QuoteError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
QuoteError::IoError(err) => write!(f, "IoError: {err:?}"),
|
||||
QuoteError::IntelQuoteLibrary(err) => {
|
||||
write!(f, "IntelQuoteLibrary: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for QuoteError {
|
||||
fn from(err: io::Error) -> QuoteError {
|
||||
QuoteError::IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
enum QuoteError {
|
||||
IoError(io::Error),
|
||||
IntelQuoteLibrary(QlError),
|
||||
}
|
||||
|
||||
impl From<QlError> for QuoteError {
|
||||
fn from(src: QlError) -> Self {
|
||||
Self::IntelQuoteLibrary(src)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
async fn gramine_quote(public_key: Vec<u8>) -> Result<Quote, QuoteError> {
|
||||
//// Check if the the gramine pseudo-hardware exists
|
||||
if !Path::new("/dev/attestation/quote").exists() {
|
||||
return Ok(Quote::default());
|
||||
}
|
||||
|
||||
// Reading attestation type
|
||||
let mut attestation_file = File::open("/dev/attestation/attestation_type")?;
|
||||
let mut attestation_type = String::new();
|
||||
attestation_file.read_to_string(&mut attestation_type)?;
|
||||
debug!("Detected attestation type: {}", attestation_type);
|
||||
|
||||
// Read `/dev/attestation/my_target_info`
|
||||
let my_target_info = fs::read("/dev/attestation/my_target_info")?;
|
||||
|
||||
// Write to `/dev/attestation/target_info`
|
||||
fs::write("/dev/attestation/target_info", my_target_info)?;
|
||||
|
||||
//// Writing the pubkey to bind the instance to the hw (note: this is not
|
||||
//// mrsigner)
|
||||
fs::write("/dev/attestation/user_report_data", public_key)?;
|
||||
|
||||
//// Reading from the gramine quote pseudo-hardware `/dev/attestation/quote`
|
||||
let mut quote_file = File::open("/dev/attestation/quote")?;
|
||||
let mut quote = Vec::new();
|
||||
let _ = quote_file.read_to_end(&mut quote);
|
||||
//// todo: wire up Qlerror and drop .expect()
|
||||
let quote3 = Quote3::try_from(quote.as_ref()).expect("quote3 error");
|
||||
let mrenclave = quote3.app_report_body().mr_enclave().to_string();
|
||||
let mrsigner = quote3.app_report_body().mr_signer().to_string();
|
||||
|
||||
debug!("mrenclave: {}", mrenclave);
|
||||
debug!("mrsigner: {}", mrsigner);
|
||||
|
||||
//// Return the Quote struct with the extracted data
|
||||
Ok(Quote {
|
||||
raw_quote: Some(hex::encode(quote)),
|
||||
mrsigner: Some(mrsigner),
|
||||
mrenclave: Some(mrenclave),
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn quote(public_key: Vec<u8>) -> Quote {
|
||||
//// tee-detection logic will live here, for now its only gramine-sgx
|
||||
match gramine_quote(public_key).await {
|
||||
Ok(quote) => quote,
|
||||
Err(err) => {
|
||||
error!("Failed to retrieve quote: {:?}", err);
|
||||
match err {
|
||||
QuoteError::IoError(_) => Quote {
|
||||
raw_quote: None,
|
||||
mrsigner: None,
|
||||
mrenclave: None,
|
||||
error: Some("io".to_owned()),
|
||||
},
|
||||
QuoteError::IntelQuoteLibrary(_) => Quote {
|
||||
raw_quote: None,
|
||||
mrsigner: None,
|
||||
mrenclave: None,
|
||||
error: Some("hw".to_owned()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tlsn::attestation::CryptoProvider;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
#[cfg(feature = "tee_quote")]
|
||||
use crate::tee::Quote;
|
||||
use crate::{auth::AuthorizationMode, config::NotarizationProperties};
|
||||
|
||||
/// Response object of the /info API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InfoResponse {
|
||||
/// Current version of notary-server
|
||||
pub version: String,
|
||||
/// Public key of the notary signing key
|
||||
pub public_key: String,
|
||||
/// Current git commit hash of notary-server
|
||||
pub git_commit_hash: String,
|
||||
/// Hardware attestation
|
||||
#[cfg(feature = "tee_quote")]
|
||||
pub quote: Quote,
|
||||
}
|
||||
|
||||
/// Request query of the /notarize API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotarizationRequestQuery {
|
||||
/// Session id that is returned from /session API
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
/// Global data that needs to be shared with the axum handlers
|
||||
#[derive(Clone)]
|
||||
pub struct NotaryGlobals {
|
||||
pub crypto_provider: Arc<CryptoProvider>,
|
||||
pub notarization_config: NotarizationProperties,
|
||||
/// A temporary storage to store session_id
|
||||
pub store: Arc<Mutex<HashMap<String, ()>>>,
|
||||
/// Selected authorization mode if any
|
||||
pub authorization_mode: Option<AuthorizationMode>,
|
||||
/// A semaphore to acquire a permit for notarization
|
||||
pub semaphore: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl NotaryGlobals {
|
||||
pub fn new(
|
||||
crypto_provider: Arc<CryptoProvider>,
|
||||
notarization_config: NotarizationProperties,
|
||||
authorization_mode: Option<AuthorizationMode>,
|
||||
semaphore: Arc<Semaphore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
crypto_provider,
|
||||
notarization_config,
|
||||
store: Default::default(),
|
||||
authorization_mode,
|
||||
semaphore,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use eyre::{eyre, Result};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::path::Path;
|
||||
|
||||
/// Parse a yaml configuration file into a struct
|
||||
pub fn parse_config_file<T: DeserializeOwned>(location: &str) -> Result<T> {
|
||||
let file = std::fs::File::open(location)?;
|
||||
let config: T = serde_yaml::from_reader(file)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Parse a csv file into a vec of structs
|
||||
pub fn parse_csv_file<T: DeserializeOwned>(location: &str) -> Result<Vec<T>> {
|
||||
let file = std::fs::File::open(location)?;
|
||||
let mut reader = csv::Reader::from_reader(file);
|
||||
let mut table: Vec<T> = Vec::new();
|
||||
for result in reader.deserialize() {
|
||||
let record: T = result?;
|
||||
table.push(record);
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
/// Prepend a file path with a base directory if the path is not absolute.
|
||||
pub fn prepend_file_path<S: AsRef<str>>(file_path: S, base_dir: S) -> Result<String> {
|
||||
let path = Path::new(file_path.as_ref());
|
||||
if !path.is_absolute() {
|
||||
Ok(Path::new(base_dir.as_ref())
|
||||
.join(path)
|
||||
.to_str()
|
||||
.ok_or_else(|| eyre!("Failed to convert path to str"))?
|
||||
.to_string())
|
||||
} else {
|
||||
Ok(file_path.as_ref().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use crate::{
|
||||
auth::AuthorizationWhitelistRecord,
|
||||
config::NotaryServerProperties,
|
||||
util::{parse_csv_file, prepend_file_path},
|
||||
};
|
||||
|
||||
use super::{parse_config_file, Result};
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_file() {
|
||||
let location = "../tests-integration/fixture/config/config.yaml";
|
||||
let config: Result<NotaryServerProperties> = parse_config_file(location);
|
||||
assert!(
|
||||
config.is_ok(),
|
||||
"Could not open file or read the file's values."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_csv_file() {
|
||||
let location = "../tests-integration/fixture/auth/whitelist.csv";
|
||||
let table: Result<Vec<AuthorizationWhitelistRecord>> = parse_csv_file(location);
|
||||
assert!(
|
||||
table.is_ok(),
|
||||
"Could not open csv or read the csv's values."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepend_file_path() {
|
||||
let base_dir = "/base/dir";
|
||||
let relative_path = "relative/path";
|
||||
let absolute_path = "/absolute/path";
|
||||
|
||||
let result = prepend_file_path(relative_path, base_dir);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "/base/dir/relative/path");
|
||||
|
||||
let result = prepend_file_path(absolute_path, base_dir);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "/absolute/path");
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
This folder contains the necessary files to build a Docker image for running the Notary Server on Intel SGX-enabled hardware.
|
||||
|
||||
## Compile the Notary Server for Intel SGX
|
||||
|
||||
We use [Gramine](https://github.com/gramineproject/gramine) to run the Notary Server on Intel SGX. Gramine allows the Notary Server to run in an isolated environment with minimal host requirements.
|
||||
|
||||
The isolated environment is defined via the manifest template (`notary-server.manifest.template`).
|
||||
|
||||
The Notary Server for SGX is compiled with the Rust feature flag `tee_quote`. This enables the server to add the SGX *quote* to the server's `/info` endpoint.
|
||||
|
||||
### CI
|
||||
|
||||
The [notary-server-sgx Docker container](https://github.com/tlsnotary/tlsn/pkgs/container/tlsn%2Fnotary-server-sgx) is built as part of the CI pipeline. For details on the build process, refer to the [CI workflow configuration](../../../../.github/workflows/ci.yml).
|
||||
|
||||
CI builds a zip file named `notary-server-sgx.zip`, which contains the compiled binary and the signed manifest. This zip file is available for all releases and `dev` builds in the build artifacts. We also publish a Docker image `notary-server-sgx` at <https://github.com/tlsnotary/tlsn/pkgs/container/tlsn%2Fnotary-server-sgx>. Check the section below for details on running this container.
|
||||
|
||||
### Development
|
||||
|
||||
You can also build everything locally using the `run-gramine-local.sh` script.
|
||||
|
||||
This script creates and signs the Gramine manifest for the Notary Server in a local development environment. It requires the Gramine SDK, so the most convenient way to use it is within a Docker container that includes the necessary dependencies and tools.
|
||||
|
||||
> ⚠️ This script assumes that the `notary-server` binary is already built (for `linux/amd64`) and available in the current directory. Make sure it is built with the `tee_quote` feature:
|
||||
> `cargo build --bin notary-server --release --features tee_quote`
|
||||
|
||||
#### Build the Docker Image
|
||||
|
||||
To build the Docker image for local development, run:
|
||||
```sh
|
||||
docker build -f gramine-local.Dockerfile -t gramine-local .
|
||||
```
|
||||
#### Run the Gramine Script
|
||||
|
||||
Once the image is built, you can run the `run-gramine-local.sh` script inside the container:
|
||||
```
|
||||
docker run --rm -it \
|
||||
--platform=linux/amd64 \
|
||||
-v "${PWD}:/app" \
|
||||
-w /app/ \
|
||||
gramine-local \
|
||||
"bash -c ./run-gramine-local.sh"
|
||||
```
|
||||
|
||||
If successful, the script will generate the following files:
|
||||
* `notary-server.sig`
|
||||
* `notary-server-sigstruct.json`
|
||||
* `notary-server.manifest`
|
||||
* `notary-server.manifest.sgx`
|
||||
|
||||
|
||||
You can verify that the provided **enclave signature (`notary-server.sig`)** matches the expected **`MR_ENCLAVE` and `MR_SIGNER`** values in `notary-server-sigstruct.json`, by running the following command inside a **Gramine Docker container** to inspect the enclave's signature:
|
||||
|
||||
```sh
|
||||
docker run --rm -v "$(pwd):/work" -w /work gramineproject/gramine:latest \
|
||||
"gramine-sgx-sigstruct-view --verbose --output-format=json notary-server.sig"
|
||||
```
|
||||
|
||||
The output should be the same as `notary-server-sigstruct.json`
|
||||
|
||||
## How to Run TLSNotary on Intel SGX?
|
||||
|
||||
Before running the Notary Server on Intel SGX hardware, ensure your system has the required Intel SGX components installed:
|
||||
```sh
|
||||
wget https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key
|
||||
cat intel-sgx-deb.key | sudo tee /etc/apt/keyrings/intel-sgx-keyring.asc > /dev/null
|
||||
|
||||
# Add the repository to your sources:
|
||||
echo 'deb [signed-by=/etc/apt/keyrings/intel-sgx-keyring.asc arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu noble main' | sudo tee /etc/apt/sources.list.d/intel-sgx.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsgx-epid libsgx-quote-ex libsgx-dcap-ql -y
|
||||
```
|
||||
|
||||
For more details, refer to the official **[Intel SGX Installation Guide](https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_SGX_SW_Installation_Guide_for_Linux.pdf).**
|
||||
|
||||
### Docker Compose
|
||||
|
||||
To run the Notary Server using Docker Compose, create a docker-compose.yml file like the following:
|
||||
```yaml
|
||||
services:
|
||||
dev:
|
||||
container_name: dev
|
||||
image: ghcr.io/tlsnotary/tlsn/notary-server-sgx:dev
|
||||
restart: unless-stopped
|
||||
devices:
|
||||
- /dev/sgx_enclave
|
||||
- /dev/sgx_provision
|
||||
volumes:
|
||||
- /var/run/aesmd/aesm.socket:/var/run/aesmd/aesm.socket
|
||||
ports:
|
||||
- "7047:7047"
|
||||
entrypoint: [ "gramine-sgx", "notary-server" ]
|
||||
```
|
||||
|
||||
To retrieve the SGX attestation quote, query the `/info` endpoint:
|
||||
```sh
|
||||
curl localhost:7047/info | jq
|
||||
```
|
||||
|
||||
### Run local build directly with Gramine
|
||||
|
||||
To run a locally built Notary Server inside a Gramine-protected SGX enclave, execute:
|
||||
```sh
|
||||
docker run --detach \
|
||||
--restart=unless-stopped \
|
||||
--device=/dev/sgx_enclave \
|
||||
--device=/dev/sgx_provision \
|
||||
--volume=/var/run/aesmd/aesm.socket:/var/run/aesmd/aesm.socket \
|
||||
--publish=7047:7047 \
|
||||
--volume="$(pwd):/work" \
|
||||
--workdir=/work \
|
||||
gramineproject/gramine:latest \
|
||||
"gramine-sgx notary-server"
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM --platform=linux/amd64 gramineproject/gramine:latest
|
||||
|
||||
RUN apt update && \
|
||||
apt install -y jq openssl zip && \
|
||||
apt clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
@@ -1,11 +0,0 @@
|
||||
FROM gramineproject/gramine:latest
|
||||
WORKDIR /work
|
||||
|
||||
# Copies `notary-server-sgx.zip` from the CI build or created locally via `run-gramine-local.sh`.
|
||||
COPY ./notary-server-sgx /work
|
||||
RUN chmod +x /work/notary-server
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/tlsnotary/tlsn
|
||||
LABEL org.opencontainers.image.description="TLSNotary notary server in SGX/Gramine."
|
||||
|
||||
ENTRYPOINT ["gramine-sgx", "notary-server"]
|
||||
@@ -1,38 +0,0 @@
|
||||
libos.entrypoint = "{{ self_exe }}"
|
||||
loader.log_level = "{{ log_level }}"
|
||||
|
||||
loader.env.LD_LIBRARY_PATH = "/lib:{{ arch_libdir }}"
|
||||
|
||||
# See https://gramine.readthedocs.io/en/stable/performance.html#glibc-malloc-tuning
|
||||
loader.env.MALLOC_ARENA_MAX = "1"
|
||||
|
||||
# encrypted type not used
|
||||
fs.mounts = [
|
||||
{ path = "/lib", uri = "file:{{ gramine.runtimedir() }}" },
|
||||
{ path = "{{ arch_libdir }}", uri = "file:{{ arch_libdir }}" },
|
||||
{ type = "tmpfs", path = "/ephemeral" },
|
||||
{ type = "encrypted", path = "/vault", uri = "file:vault", key_name = "_sgx_mrenclave" },
|
||||
|
||||
]
|
||||
|
||||
# hashed @ buildtime. at runtime => these files are +ro
|
||||
# and can be accessed if hash matches manifest
|
||||
# !!!! hashed !!!!
|
||||
# https://gramine.readthedocs.io/en/stable/manifest-syntax.html#trusted-files
|
||||
sgx.trusted_files = [
|
||||
"file:{{ self_exe }}",
|
||||
"file:{{ gramine.runtimedir() }}/",
|
||||
"file:{{ arch_libdir }}/",
|
||||
]
|
||||
|
||||
sgx.edmm_enable = false
|
||||
sgx.remote_attestation = "dcap"
|
||||
sgx.max_threads = 64
|
||||
sgx.enclave_size = "2G"
|
||||
sys.disallow_subprocesses = true
|
||||
|
||||
|
||||
#### tlsn rev
|
||||
sgx.isvprodid = 7
|
||||
#### F
|
||||
sgx.isvsvn = 1
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "[*] Generating SGX signing key..."
|
||||
gramine-sgx-gen-private-key
|
||||
|
||||
if [ ! -f notary-server ]; then
|
||||
echo "[!] notary-server binary not found. Please copy it from ci, or build it first."
|
||||
echo "Note that notary-server must be built for linux/amd64 with tee_quote feature enabled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x notary-server
|
||||
|
||||
echo "[*] Creating Gramine manifest..."
|
||||
gramine-manifest \
|
||||
-Dlog_level=debug \
|
||||
-Darch_libdir=/lib/x86_64-linux-gnu \
|
||||
-Dself_exe=notary-server \
|
||||
notary-server.manifest.template \
|
||||
notary-server.manifest
|
||||
|
||||
echo "[*] Signing manifest..."
|
||||
gramine-sgx-sign \
|
||||
--manifest notary-server.manifest \
|
||||
--output notary-server.manifest.sgx
|
||||
|
||||
echo "[*] Viewing SIGSTRUCT..."
|
||||
gramine-sgx-sigstruct-view --verbose --output-format=json notary-server.sig >notary-server-sigstruct.json
|
||||
|
||||
cat notary-server-sigstruct.json | jq .
|
||||
|
||||
mr_enclave=$(jq -r ".mr_enclave" notary-server-sigstruct.json)
|
||||
mr_signer=$(jq -r ".mr_signer" notary-server-sigstruct.json)
|
||||
|
||||
echo "=============================="
|
||||
echo "MRENCLAVE: $mr_enclave"
|
||||
echo "MRSIGNER: $mr_signer"
|
||||
echo "=============================="
|
||||
|
||||
zip -r notary-server-sgx.zip \
|
||||
notary-server \
|
||||
notary-server-sigstruct.json \
|
||||
notary-server.sig \
|
||||
notary-server.manifest \
|
||||
notary-server.manifest.sgx \
|
||||
README.md
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "notary-tests-integration"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
notary-client = { workspace = true }
|
||||
notary-common = { workspace = true }
|
||||
notary-server = { workspace = true }
|
||||
tls-server-fixture = { workspace = true }
|
||||
tlsn = { workspace = true }
|
||||
tlsn-tls-core = { workspace = true }
|
||||
tlsn-core = { workspace = true }
|
||||
|
||||
async-tungstenite = { workspace = true, features = ["tokio-native-tls"] }
|
||||
futures = { workspace = true }
|
||||
http = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hyper = { workspace = true, features = ["client", "http1", "server"] }
|
||||
hyper-tls = { version = "0.6", features = [
|
||||
"vendored",
|
||||
] } # specify vendored feature to use statically linked copy of OpenSSL
|
||||
hyper-util = { workspace = true, features = ["full"] }
|
||||
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
|
||||
rstest = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
rustls-pemfile = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-native-tls = { version = "0.3.1", features = ["vendored"] }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
uuid = { workspace = true, features = ["v4", "fast-rng"] }
|
||||
ws_stream_tungstenite = { workspace = true, features = ["tokio_io"] }
|
||||
@@ -1 +0,0 @@
|
||||
!*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user