-
-
Save denniswon/c14da11214d9a5a9d0bb18d22c56f77a to your computer and use it in GitHub Desktop.
| --- | |
| TLSNotary Integration — Complete Technical State Analysis | |
| 1. Architecture Overview | |
| The TLSNotary integration adds zkTLS (MPC-TLS based proof of web data) to Newton Protocol. It enables operators to | |
| cryptographically verify that data was fetched from a specific HTTPS API without ever seeing the user's credentials. | |
| ┌─────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ | |
| │ Browser │ WS │ Newton │ TCP │ Target │ │ IPFS │ | |
| │ Extension │────▶│ Sidecar │────▶│ API │ │ Gateway │ | |
| │ (Prover) │ │ (Verifier) │ │ Server │ │ │ | |
| └──────┬───────┘ └──────────────┘ └──────────┘ └──────┬───────┘ | |
| │ │ | |
| │ 1. MPC-TLS + Attestation │ | |
| │ 2. Create Presentation (selective disclosure) │ | |
| │ 3. Upload to IPFS ─────────────────────────────────────▶│ | |
| │ │ | |
| │ 4. Submit task with proof_cid │ | |
| ▼ │ | |
| ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ | |
| │ Gateway │────▶│ Operators │────▶│ Aggregator │ │ | |
| │ (proof_cid │ │ (fetch CID, │ │ (BLS agg) │ │ | |
| │ passthru) │ │ verify, │◀────┘ │ │ | |
| └──────────────┘ │ Rego eval) │ │ │ | |
| │ │◀────────────────────────────┘ | |
| └──────────────┘ 5. Fetch presentation by CID | |
| ★ Insight ───────────────────────────────────── | |
| Two verification paths exist — and this is important for your review: | |
| 1. Operator-level (crates/operator/src/tls_proof.rs): Strict trusted-key verification. Reads TLSN_TRUSTED_NOTARY_KEY from env. | |
| Rejects presentations signed by unknown notaries. | |
| 2. WASM data-provider level (crates/data-provider/src/wasm/tlsn.rs): Trusts the presentation's embedded key. The security | |
| boundary here is the WASM sandbox itself, not the notary key. This means a malicious WASM plugin could accept any notary. | |
| These two paths serve different use cases, but the inconsistency is worth flagging. | |
| ───────────────────────────────────────────────── | |
| 2. Component-by-Component Breakdown | |
| 2.1 Core Library: crates/tls-notary/ (Phase 1 — MERGED to main) | |
| Files: lib.rs, prove.rs, present.rs, verify.rs | |
| Current State: On main, working. | |
| ┌───────────────────────┬───────────────────────────────────┬────────────────────────────────────────────────────────────────┐ | |
| │ Function │ Purpose │ Key Details │ | |
| ├───────────────────────┼───────────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ notarize() │ MPC-TLS prover + verifier over │ Production flow requires WebSocket (sidecar), not duplex │ | |
| │ │ in-memory duplex │ │ | |
| ├───────────────────────┼───────────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ create_presentation() │ Selective disclosure from │ Auto-redacts Authorization, Cookie, User-Agent, X-API-Key, │ | |
| │ │ attestation │ Proxy-Authorization, Set-Cookie │ | |
| ├───────────────────────┼───────────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ verify_presentation() │ Verify against trusted notary key │ Requires explicit trusted_key param (good security) │ | |
| ├───────────────────────┼───────────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ get_crypto_provider() │ Root CA selection │ Compile-time gated via #[cfg(feature = "fixtures")] │ | |
| └───────────────────────┴───────────────────────────────────┴────────────────────────────────────────────────────────────────┘ | |
| Technical Details I Found: | |
| - Serialization: Uses bincode 2.x serde API (bincode::serde::encode_to_vec / decode_from_slice) — but on the phase3 branch, the | |
| README shows bincode::serialize() (bincode 1.x API). This is a version inconsistency between what's on main (bincode 2.x) and | |
| what's documented on later branches. | |
| - OOM Protection: MAX_ATTESTATION_SIZE = 1 MiB, uses futures::AsyncReadExt::take() on all unbounded reads. This was a review | |
| feedback item you requested — implemented correctly. | |
| - Signing Key Safety: run_verifier() uses [1u8; 32] test key, gated behind #[cfg(any(test, feature = "fixtures"))]. Production | |
| builds error without a key. Good. | |
| - In-memory duplex: prove.rs uses tokio::io::duplex(MAX_RECV_BUFFER) for prover ↔ verifier. This only works for testing. The | |
| sidecar uses WebSocket for the real flow. The notarize() function is therefore test-only in its current form — it cannot be used | |
| in production as-is because the verifier must be a remote sidecar. | |
| 2.2 Sidecar Binary: bin/tlsn-sidecar/ (Phase 2 — PR #379, NOT merged) | |
| Files: main.rs, session.rs, verifier_core.rs, config.rs, egress.rs, auth.rs, proof.rs, rate_limit.rs, ws_adapter.rs | |
| This is the production notary/verifier server. 9 source files, ~1800 lines. | |
| Endpoints: | |
| ┌─────────────────┬────────┬──────┬─────────────────────────────────────────────────────┐ | |
| │ Route │ Method │ Auth │ Purpose │ | |
| ├─────────────────┼────────┼──────┼─────────────────────────────────────────────────────┤ | |
| │ /health │ GET │ No │ Health + Prometheus metrics │ | |
| ├─────────────────┼────────┼──────┼─────────────────────────────────────────────────────┤ | |
| │ /session │ WS │ No │ Create MPC-TLS session, returns sessionId │ | |
| ├─────────────────┼────────┼──────┼─────────────────────────────────────────────────────┤ | |
| │ /verifier │ WS │ No │ Prover connects here for MPC-TLS verification │ | |
| ├─────────────────┼────────┼──────┼─────────────────────────────────────────────────────┤ | |
| │ /proxy │ WS │ No │ WebSocket→TCP bridge (session-bound, SSRF-filtered) │ | |
| ├─────────────────┼────────┼──────┼─────────────────────────────────────────────────────┤ | |
| │ /v1/proof/store │ POST │ Yes │ Upload presentation to IPFS │ | |
| ├─────────────────┼────────┼──────┼─────────────────────────────────────────────────────┤ | |
| │ /v1/proof/:cid │ GET │ No │ Retrieve presentation from IPFS │ | |
| └─────────────────┴────────┴──────┴─────────────────────────────────────────────────────┘ | |
| Issues and Concerns I Found: | |
| Security | |
| 1. /session and /verifier have no authentication. Anyone can create MPC-TLS sessions. The session endpoints are unauthenticated — | |
| only /v1/proof/store requires API key auth. This means an attacker can exhaust the session pool (bounded at max_sessions=256) | |
| without any credentials. | |
| 2. Auth middleware is not applied as a layer. The auth.rs defines extract_bearer_token() and AuthProvider but they're only called | |
| manually in proof.rs::store_proof_handler. There's no Tower middleware extracting/validating auth. The auth.rs file has unused | |
| middleware imports and a manual check pattern. | |
| 3. Proxy endpoint session binding is weak. The /proxy endpoint requires a session parameter that must exist in the session map, | |
| but there's no crypto binding between the session creator and the proxy user. Anyone who knows (or guesses) a session ID can use | |
| the proxy. | |
| 4. CORS is fully open: CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any). For a browser extension talking | |
| to localhost this makes sense, but if deployed as a shared service, this is too permissive. | |
| 5. verify_reveal_config uses sent_unsafe()/received_unsafe() after validating ranges. The comment explains why, but this is | |
| fragile — the validation must always run before extraction. | |
| Architecture | |
| 6. ws_adapter.rs is a full reimplementation of axum's WebSocket. This is ~300 lines of custom WebSocket upgrade, frame handling, | |
| and Sink/Stream impls. This exists because TLSNotary needs futures::AsyncRead/AsyncWrite (which async-tungstenite provides) while | |
| axum uses tokio-tungstenite. This is a significant maintenance burden. | |
| 7. No signing key management for the sidecar verifier. The sidecar's verifier_core.rs uses RootCertStore::mozilla() for TLS | |
| verification but doesn't configure a signing key. The attestation signing happens in prove.rs::run_verifier() which isn't used by | |
| the sidecar — the sidecar delegates to Session::new() + handle.new_verifier(). The sidecar doesn't produce attestations — it | |
| runs the MPC-TLS protocol and produces VerifierOutput with transcript data. The extension/prover then builds the attestation | |
| using its own key? This flow needs clarification. | |
| 8. Session state is in-memory HashMap behind a Mutex. No persistence, no clustering. Sidecar must be single-instance per client. | |
| Session reaper runs every 60 seconds. | |
| 9. IPFS integration has no pinning. upload_to_ipfs calls /api/v0/add but doesn't pin. Garbage collection could remove proofs | |
| before operators verify them. | |
| Error Handling | |
| 10. eyre used throughout sidecar instead of thiserror. Only verifier_core.rs and egress.rs use typed thiserror errors. main.rs, | |
| proof.rs, config.rs all use eyre::Result. Per project conventions (rust.md), binaries can use anyhow/eyre at entry points, but | |
| library-like modules should use typed errors. | |
| 11. data_provider/src/wasm/tlsn.rs uses bcs for deserialization while the rest of the codebase uses bincode. | |
| verify_presentation_bytes calls bcs::from_bytes() — this is a serialization format mismatch. If the extension serializes with | |
| bincode and the data provider deserializes with BCS, it will fail. | |
| Performance | |
| 12. reqwest::Client::new() in AppState. Good — shared client with connection pooling. | |
| 13. Rate limiter uses governor with DashMap. Per-IP limiting is implemented. Global limiting is marked "reserved, not yet | |
| enforced" in the config comments. | |
| 14. No connection timeout on IPFS requests. upload_to_ipfs and get_proof_handler use the shared reqwest::Client without explicit | |
| timeouts on individual requests. | |
| 2.3 Operator Integration (Phase 2-3 — NOT merged) | |
| Files: crates/operator/src/tls_proof.rs, changes to core.rs and builder.rs | |
| Flow: | |
| 1. Operator receives task with proof_cid | |
| 2. Fetches presentation from IPFS via fetch_and_verify_tls_proof() | |
| 3. Deserializes with bincode | |
| 4. Verifies against TLSN_TRUSTED_NOTARY_KEY | |
| 5. Extracts VerifiedTlsProof { server_name, verified, response_body, request_target } | |
| 6. Injects into Rego as data.tls_proof namespace | |
| 7. Policy evaluation proceeds with TLS data available | |
| Issues: | |
| 15. Feature-gated behind tls-notary. All TLSN code is #[cfg(feature = "tls-notary")] with fallback to None. This is clean. | |
| 16. Trusted key from env var: TLSN_TRUSTED_NOTARY_KEY. No rotation, no multi-key support, no on-chain registry. If the notary key | |
| changes, all operators must update their env vars. | |
| 17. Soft failure on verification: When proof verification fails, operator logs a warning and proceeds without the TLS proof data. | |
| The policy may still evaluate to true if it doesn't require TLS data. This could be a security concern — a task with proof_cid | |
| that fails verification should arguably be rejected, not evaluated without the proof. | |
| 18. HTTP response body parsing is naive: tls_proof.rs:131-137 parses the HTTP body by finding \r\n\r\n. This doesn't handle | |
| chunked transfer encoding, Content-Encoding (gzip), or multi-part responses. | |
| 19. get_trusted_notary_key() tests use env::set_var/env::remove_var. Per testing.md, this is a test anti-pattern — env vars are | |
| process-wide and tests run in parallel. These tests are racy. | |
| 2.4 Data Provider WASM Integration (Phase 3 — NOT merged) | |
| Files: crates/data-provider/src/wasm/tlsn.rs, crates/data-provider/wit-v2/newton-provider.wit | |
| WIT Interface (@0.2.0): | |
| interface tlsn { | |
| verify-from-cid: func(proof-cid: string) -> result<verified-data, string>; | |
| verify: func(presentation-bytes: list<u8>) -> result<verified-data, string>; | |
| } | |
| Issues: | |
| 20. CID multihash verification only supports SHA2-256 (code 0x12). CIDv1 with Blake2b or other hash functions will be rejected. | |
| This is acceptable for now but limits future IPFS compatibility. | |
| 21. verify_presentation_bytes trusts the embedded notary key. This is documented but weaker than the operator-level check. A | |
| malicious WASM plugin could construct a self-signed presentation and it would pass verification here. | |
| 22. BCS vs bincode inconsistency (critical). tlsn.rs:119 uses bcs::from_bytes() but the core library serializes with | |
| bincode::serde::encode_to_vec(). These are incompatible formats. This will cause deserialization failures at runtime. | |
| 2.5 Gateway Integration (Phase 3-5 — NOT merged) | |
| Changes: proof_cid field added to CreateTaskRequest and SendTaskRequest, threaded through ConsensusCommitRequest, passed to | |
| operators. | |
| Issues: | |
| 23. proof_cid on SendTaskRequest says "Not supported in async mode; returns an error if provided" — but I don't see the actual | |
| rejection logic. Just the doc comment. | |
| 24. No validation of proof_cid format. The gateway passes through whatever string the client sends. No CID format validation, no | |
| length check. | |
| 25. Gateway doesn't verify the proof itself. It's a pure passthrough. This means the gateway can't reject obviously invalid CIDs | |
| before broadcasting to operators, wasting operator compute. | |
| 2.6 Rego Policy Integration (Phase 5 — NOT merged) | |
| Three policy templates: | |
| - tlsn_twitter_followers.rego — Twitter/X follower count check | |
| - tlsn_bank_balance.rego — Bank balance + currency verification | |
| - tlsn_generic.rego — Configurable field checks with comparison operators (gt, gte, lt, lte, eq) | |
| Data namespace: data.tlsn_verified, data.tlsn_server, data.tlsn_connection_time, data.tlsn_data | |
| Issues: | |
| 26. Proof freshness checked in Rego via max_proof_age_secs. This is good but relies on the connection timestamp from the | |
| presentation, which is set by the notary. A malicious notary could backdate or future-date this. | |
| 27. E2E tests are all mock-based. No real MPC-TLS, no real IPFS. The tests verify Rego policy logic against hardcoded JSON, not | |
| the actual verification pipeline. | |
| 3. Cross-Cutting Concerns | |
| 3.1 Serialization Format Mismatch (Critical) | |
| ┌─────────────────────────┬────────────────────┬────────────────────┐ | |
| │ Component │ Serialize │ Deserialize │ | |
| ├─────────────────────────┼────────────────────┼────────────────────┤ | |
| │ prove.rs (core) │ bincode 2.x serde │ bincode 2.x serde │ | |
| ├─────────────────────────┼────────────────────┼────────────────────┤ | |
| │ proof.rs (sidecar) │ base64 passthrough │ base64 passthrough │ | |
| ├─────────────────────────┼────────────────────┼────────────────────┤ | |
| │ tls_proof.rs (operator) │ — │ bincode 2.x serde │ | |
| ├─────────────────────────┼────────────────────┼────────────────────┤ | |
| │ tlsn.rs (data-provider) │ — │ bcs │ | |
| └─────────────────────────┴────────────────────┴────────────────────┘ | |
| The data-provider path uses BCS while everything else uses bincode. This is a runtime failure waiting to happen. | |
| 3.2 Trust Model Inconsistencies | |
| ┌───────────────────────┬──────────────────────────────┬─────────────────────────────────┐ | |
| │ Path │ Trust Level │ Key Source │ | |
| ├───────────────────────┼──────────────────────────────┼─────────────────────────────────┤ | |
| │ Operator tls_proof.rs │ Strict: explicit trusted key │ TLSN_TRUSTED_NOTARY_KEY env var │ | |
| ├───────────────────────┼──────────────────────────────┼─────────────────────────────────┤ | |
| │ WASM tlsn.rs │ Permissive: embedded key │ Presentation itself │ | |
| ├───────────────────────┼──────────────────────────────┼─────────────────────────────────┤ | |
| │ verify.rs (core) │ Strict: caller provides key │ Function parameter │ | |
| └───────────────────────┴──────────────────────────────┴─────────────────────────────────┘ | |
| 3.3 Feature Gate Matrix | |
| ┌────────────────────────┬────────────┬──────────────────────────────────────┐ | |
| │ Crate │ Feature │ What it gates │ | |
| ├────────────────────────┼────────────┼──────────────────────────────────────┤ | |
| │ newton-tls-notary │ fixtures │ Test CA certificates │ | |
| ├────────────────────────┼────────────┼──────────────────────────────────────┤ | |
| │ newton-prover-operator │ tls-notary │ All TLSN verification code │ | |
| ├────────────────────────┼────────────┼──────────────────────────────────────┤ | |
| │ newton-prover-core │ (none) │ Rego integration is always available │ | |
| └────────────────────────┴────────────┴──────────────────────────────────────┘ | |
| 3.4 Dependency Chain | |
| bin/tlsn-sidecar | |
| └── tlsn (via libs/tlsn submodule, alpha.14) | |
| └── newton-prover-core (for hex! macro) | |
| └── newton-prover-metrics | |
| crates/tls-notary | |
| └── tlsn (prove, verify, present) | |
| └── tlsn-formats (HTTP transcript parsing) | |
| crates/operator (feature = "tls-notary") | |
| └── crates/tls-notary | |
| └── newton-prover-core (fetch_from_ipfs) | |
| crates/data-provider | |
| └── crates/tls-notary (verify only) | |
| └── bcs (!!!) | |
| 4. Summary of Issues for PR #379 Review | |
| Critical (Must Fix) | |
| ┌─────┬──────────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────┐ | |
| │ # │ Issue │ Location │ | |
| ├─────┼──────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────┤ | |
| │ 22 │ BCS vs bincode serialization mismatch in data-provider │ data-provider/src/wasm/tlsn.rs:119 │ | |
| ├─────┼──────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────┤ | |
| │ 17 │ Soft failure on proof verification — operator proceeds without TLS data when │ operator/src/core.rs │ | |
| │ │ verification fails, potentially allowing bypass │ │ | |
| └─────┴──────────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────┘ | |
| High (Should Fix) | |
| ┌─────┬────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────┐ | |
| │ # │ Issue │ Location │ | |
| ├─────┼────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────┤ | |
| │ 1 │ /session and /verifier endpoints unauthenticated — session pool exhaustion DoS │ sidecar/src/main.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────┤ | |
| │ 3 │ Proxy session binding has no crypto proof — session ID guessable (UUID v4, but no MAC) │ sidecar/src/session.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────┤ | |
| │ 7 │ Sidecar verifier doesn't produce attestations — flow unclear for production use │ sidecar/src/verifier_core.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────┤ | |
| │ 14 │ No request-level timeouts on IPFS fetches │ sidecar/src/proof.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────┤ | |
| │ 19 │ env::set_var in tests (racy) │ operator/src/tls_proof.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────┤ | |
| │ 24 │ No CID format validation in gateway │ gateway/src/rpc/types/mod.rs │ | |
| └─────┴────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────┘ | |
| Medium (Consider) | |
| ┌─────┬────────────────────────────────────────────────────────────┬────────────────────────────────┐ | |
| │ # │ Issue │ Location │ | |
| ├─────┼────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 6 │ 300-line custom WebSocket adapter — maintenance burden │ sidecar/src/ws_adapter.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 9 │ IPFS upload without pinning — GC could remove proofs │ sidecar/src/proof.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 16 │ Single trusted key via env var — no rotation/multi-key │ operator/src/tls_proof.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 18 │ Naive HTTP body parsing (no chunked/gzip support) │ operator/src/tls_proof.rs:131 │ | |
| ├─────┼────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 21 │ WASM path trusts embedded notary key — weaker security │ data-provider/src/wasm/tlsn.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 23 │ proof_cid "not supported in async mode" but no enforcement │ gateway/src/rpc/types/mod.rs │ | |
| ├─────┼────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 26 │ Proof freshness relies on notary-set timestamp │ Rego policies │ | |
| └─────┴────────────────────────────────────────────────────────────┴────────────────────────────────┘ | |
| Low / Observations | |
| ┌─────┬─────────────────────────────────────────────────────────────────┬────────────────────────────────┐ | |
| │ # │ Issue │ Location │ | |
| ├─────┼─────────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 4 │ CORS fully open (fine for localhost, not for shared deployment) │ sidecar/src/main.rs │ | |
| ├─────┼─────────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 8 │ Session state in-memory only — no HA/clustering │ sidecar/src/session.rs │ | |
| ├─────┼─────────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 10 │ eyre in non-entry-point modules │ Multiple sidecar files │ | |
| ├─────┼─────────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 13 │ Global rate limiting not yet enforced │ sidecar/src/config.rs │ | |
| ├─────┼─────────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 20 │ CID multihash only supports SHA2-256 │ data-provider/src/wasm/tlsn.rs │ | |
| ├─────┼─────────────────────────────────────────────────────────────────┼────────────────────────────────┤ | |
| │ 27 │ E2E tests are all mock-based — no real MPC-TLS coverage │ integration-tests/ │ | |
| └─────┴─────────────────────────────────────────────────────────────────┴────────────────────────────────┘ | |
| 5. What's Working Well | |
| - Compile-time trust boundary for test vs production crypto providers (#[cfg(feature = "fixtures")]) | |
| - OOM protection everywhere — take() on unbounded reads, MAX_PROOF_SIZE constants | |
| - SSRF defense in the sidecar — DNS rebinding guard, blocked CIDRs, session-bound proxy, byte quotas, concurrent proxy limits | |
| with RAII guards | |
| - Selective disclosure in present.rs — auto-redacts sensitive headers, supports JSON path-based field selection | |
| - Feature gating — all TLSN code is behind tls-notary feature flag, clean #[cfg] fallbacks | |
| - Typed errors in verifier_core.rs and egress.rs — proper thiserror enums | |
| - CID multihash re-verification in data-provider — defends against malicious IPFS gateways | |
| - Rate limiting with governor crate — per-IP DashMap-backed limiter with configurable burst | |
| - Session capacity limits — bounded at max_sessions with rejection when full |
Addressing Your Points
- Two Verification Paths — Design Intent vs. Implementation
You're absolutely right. The desired design is:
A single verification path through a Newton Rego extension (in libs/regorus), analogous to newton.crypto.ecdsa_recover_signer.
Something like:
Desired: single path — verify MPC-TLS proof inside Rego policy logic
tls_data := newton.crypto.tlsn_verify(input.proof_cid)
tls_data.server_name == "api.x.com"
tls_data.response.followers_count >= 1000
What @sigrid-magiclabs implemented instead is three separate paths:
- crates/operator/src/tls_proof.rs — Operator fetches + verifies before Rego eval, injects as data.tls_proof
- crates/data-provider/src/wasm/tlsn.rs — WASM host function fetches + verifies, returns to plugin
- Rego policies expect pre-verified data in data.tlsn_verified, data.tlsn_data
None of these implement the intended design of a Rego extension function. The Rego policies just check pre-injected JSON — they
don't call a verification function. This is a fundamental architectural deviation. The correct approach is:
- Add a newton.crypto.tlsn_verify(proof_cid) extension to libs/regorus (like ecdsa_recover_signer)
- This extension handles IPFS fetch, deserialization, cryptographic verification, transcript extraction — all inside the Rego
engine - Policy authors use it directly in Rego rules — single code path, no pre-injection
- Remove the operator-level and WASM-level verification paths entirely
2.1 Detailed Answers
- In-memory duplex
tokio::io::duplex(buffer_size) creates a pair of in-memory streams that are connected — bytes written to one end can be read from
the other. Think of it as an in-process pipe. In prove.rs, it connects the prover and verifier within the same process:
[Prover task] ←── duplex channel ──→ [Verifier task]
↓ ↓
(same tokio runtime, same process)
Limitation: In production, the prover is a browser extension (or client app) and the verifier is the Newton Sidecar server
running on a different machine. They can't share an in-memory pipe — they need a network transport (WebSocket). So
prove.rs::notarize() in its current form is testing-only infrastructure. The production flow must go: browser extension →
WebSocket → sidecar.
- Trusted notary key config
Agreed. The trusted notary key should be in a tls_notary.toml config file following the same pattern as gateway.toml and
operator.toml. Currently it's read from TLSN_TRUSTED_NOTARY_KEY env var, which is:
- Not discoverable (no config file documents it)
- Not consistent with the project's config pattern (TOML + env overlay)
- Hard to manage across deployments
The sidecar already has sidecar.toml — but the operator and gateway need to know the trusted key too (for the Rego extension
path). This should be a shared config section, or part of operator.toml / gateway.toml.
- Bincode version inconsistency
Agreed — must use bincode 2.x (bincode::serde::encode_to_vec / decode_from_slice) consistently everywhere. The README on phase3
referencing bincode::serialize() (1.x) is incorrect.
- In-memory duplex in production — combined gateway question
This is an important architectural question. Let me break down the tradeoffs:
Option A: Sidecar as separate service (current design)
┌──────────────────────────────────────────────────┬─────────────────────────────────────────────┐
│ Pros │ Cons │
├──────────────────────────────────────────────────┼─────────────────────────────────────────────┤
│ Isolation — sidecar crash doesn't affect gateway │ Extra deployment, networking, configuration │
├──────────────────────────────────────────────────┼─────────────────────────────────────────────┤
│ Can scale independently (MPC-TLS is CPU-heavy) │ WebSocket transport adds latency │
├──────────────────────────────────────────────────┼─────────────────────────────────────────────┤
│ Clear separation of concerns │ Client must know two endpoints │
├──────────────────────────────────────────────────┼─────────────────────────────────────────────┤
│ Browser extension connects directly to sidecar │ Extra auth surface area │
└──────────────────────────────────────────────────┴─────────────────────────────────────────────┘
Option B: TLS-notary integrated into gateway
┌──────────────────────────────────────────────────────┬──────────────────────────────────────────────────┐
│ Pros │ Cons │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Single deployment, single endpoint │ MPC-TLS CPU load competes with gateway hot paths │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Could use in-memory channels (no WebSocket overhead) │ Sidecar failure modes could cascade to gateway │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Shared auth, rate limiting, monitoring │ Increases gateway binary size and complexity │
├──────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Client only needs gateway URL │ Harder to scale the MPC-TLS piece independently │
└──────────────────────────────────────────────────────┴──────────────────────────────────────────────────┘
My assessment: The MPC-TLS protocol is CPU-intensive and long-running (seconds, not milliseconds). The gateway targets sub-100ms
RPC responses. Combining them risks violating the gateway's latency targets. Keep them separate, but:
- Share the same config patterns (TOML files)
- Share the same auth infrastructure (already done via shared api_keys table)
- The gateway should be able to proxy/redirect to the sidecar if needed
Even with separation, in-memory duplex cannot be used in production because the prover (browser) is always on a different
machine. The duplex is fundamentally test infrastructure.
2.2 Sidecar Issues
-
Security concerns — agreed, all valid for PR review.
-
ws_adapter.rs — blindly AI-generated code
I'll be direct in the review. This is a 300-line reimplementation of functionality that already exists in the ecosystem. The
file:
- Reimplements WebSocket upgrade handshake (SHA-1 signing of Sec-WebSocket-Key)
- Reimplements Stream/Sink for WebSocket frames
- Reimplements FromRequestParts for extracting upgrade requests
- Reimplements Message type with conversion to/from tungstenite
The stated reason (needing futures::AsyncRead/AsyncWrite for TLSNotary's Session::new()) is legitimate, but the solution should
be a thin adapter around an existing crate, not a ground-up reimplementation. The ws_stream_tungstenite crate (already a
dependency!) provides exactly this bridge. The comment at the top says "Adapted from tlsn-extension's axum_websocket.rs" — which
means this was copy-pasted from the TLSNotary example code, likely via AI, without evaluating whether a simpler approach exists.
This is unacceptable for production code in this project. It introduces a large, hard-to-audit surface area for a WebSocket
upgrade — something that security-critical services should not be reimplementing.
- Signing key / attestation flow — needs clarification
The flow as implemented is unclear and likely incorrect. Let me trace through what the sidecar actually does:
- session.rs: Creates a Session, calls handle.new_verifier() → commit() → accept() → run() → verify() → accept()
- verifier_core.rs: Returns (DnsName, PartialTranscript) from VerifierOutput
- Session handler extracts transcript data, maps reveal ranges, sends results
What's missing: The sidecar never builds an Attestation. It never signs anything. The VerifierOutput contains a verified
transcript, but there's no attestation document that the prover can use to create a Presentation.
Compare with prove.rs::run_verifier() which:
- Creates a Secp256k1Signer from a signing key
- Builds an AttestationConfig
- Calls Attestation::builder().accept_request(request).build(&provider)
- Sends the signed attestation back to the prover
The sidecar's verifier_core.rs does none of this. It runs the MPC-TLS protocol but doesn't produce the attestation that makes the
proof useful for operators. This is a critical gap that makes the entire sidecar flow incomplete.
This needs to be called out explicitly — the author needs to clarify the intended production attestation flow.
4-5) Session state — HashMap + Mutex
HashMap<String, SessionData> behind tokio::sync::Mutex is problematic for a concurrent WebSocket server:
- Every session creation, lookup, and cleanup acquires a full exclusive lock
- WebSocket handlers hold the lock while doing HashMap operations
- The reaper task acquires the lock every 60 seconds and iterates all entries
Better alternatives from the gateway codebase:
┌───────────────────────┬─────────────────────────────────────────┬──────────────────────────────────┐
│ Data Structure │ When to Use │ Example in Gateway │
├───────────────────────┼─────────────────────────────────────────┼──────────────────────────────────┤
│ DashMap<K, V> │ High-contention concurrent reads/writes │ Operator registry, cache │
├───────────────────────┼─────────────────────────────────────────┼──────────────────────────────────┤
│ RwLock<HashMap<K, V>> │ Read-heavy, write-light │ Config state │
├───────────────────────┼─────────────────────────────────────────┼──────────────────────────────────┤
│ Mutex<HashMap<K, V>> │ Low concurrency only │ ← current sidecar (wrong choice) │
└───────────────────────┴─────────────────────────────────────────┴──────────────────────────────────┘
For the sidecar, DashMap<String, SessionData> is the right choice — session creation and lookup are independent operations on
different keys that don't need to serialize.
- IPFS for proof storage — why?
Good question. The IPFS-based flow exists because:
- MPC-TLS proofs are large (~100KB-1MB serialized presentations). Putting them directly in RPC request payloads or on-chain is
impractical. - Content-addressable storage: The CID is a hash of the proof. Operators can independently fetch the same proof by CID and
verify it matches. No trust in the delivery channel needed. - TLSNotary ecosystem convention: The upstream TLSNotary project uses IPFS for proof distribution.
However, your intuition is valid. An alternative flow could be:
- Client uploads proof directly to gateway via RPC (e.g., newt_uploadTlsProof)
- Gateway stores it (Redis, S3, or even PostgreSQL for bounded sizes)
- Gateway assigns a reference ID and passes it to operators
- Operators fetch from gateway, not IPFS
This would eliminate the IPFS dependency and the pinning problem. The tradeoff is that the gateway becomes the storage layer
(centralization), but Newton already centralizes through the gateway for privacy data (newt_uploadEncryptedData). The same
pattern could work here.
Recommendation: Support both — direct upload to gateway (primary, for simplicity) and IPFS CID reference (optional, for
decentralized use cases). The IPFS path needs pinning regardless.
7-9) Error handling, rate limiting, timeouts — agreed, all valid.
On combining gateway + sidecar rate limiting: If we keep them separate, they should at least share the same rate limiting
infrastructure (the gateway already has governor-based limiting). If we integrate the sidecar into the gateway, this becomes
moot.
2.3 Operator Integration — agreed, all issues valid.
The operator should not be doing its own TLS proof verification. Per your design, this belongs in the Rego extension. The
operator just runs the policy engine which internally calls newton.crypto.tlsn_verify().
2.4 WASM Data Provider — same as above
The WASM data-provider path (TlsnProvider, WIT @0.2.0 tlsn interface) should also be removed in favor of the Rego extension path.
WASM plugins don't need to verify TLS proofs — the Rego engine does.
The BCS vs bincode issue is moot if this path is removed, but it still indicates the author wasn't testing their code end-to-end.
2.5 Async flow (SendTask) support
Agreed. The comment "Not supported in async mode; returns an error if provided" is incorrect. Both sync (CreateTask) and async
(SendTask) flows should support proof_cid. The proof is fetched by the Rego extension during policy evaluation regardless of
which task submission mode is used.
Consolidated Design vs. Implementation Gap
┌────────────────┬────────────────────────────────────────────┬───────────────────────────────────┬────────────────────────┐
│ Aspect │ Desired Design │ Current Implementation │ Gap │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ Verification │ Single: Rego extension │ Three paths: operator, WASM, │ Major architectural │
│ path │ newton.crypto.tlsn_verify() │ pre-injected Rego data │ deviation │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ Sidecar role │ Production MPC-TLS verifier/notary with │ Runs MPC-TLS but doesn't produce │ Critical missing │
│ │ attestation signing │ attestations │ functionality │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ Config │ TOML file per project pattern │ Env vars for trusted key │ Inconsistent │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ Serialization │ bincode 2.x everywhere │ Mixed bincode 2.x, bincode 1.x │ Multiple format │
│ │ │ docs, BCS in WASM │ mismatches │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ Async flow │ Both sync + async support proof_cid │ Async explicitly rejects it │ Design mismatch │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ IPFS │ With pinning + optional direct upload │ No pinning, IPFS-only │ Missing features │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ WebSocket │ Thin adapter or existing crate │ 300-line ground-up │ Over-engineering / AI │
│ │ │ reimplementation │ artifact │
├────────────────┼────────────────────────────────────────────┼───────────────────────────────────┼────────────────────────┤
│ Code quality │ Typed errors, project conventions │ eyre in libraries, env vars in │ Multiple convention │
│ │ │ tests, HashMap+Mutex │ violations │
└────────────────┴────────────────────────────────────────────┴───────────────────────────────────┴────────────────────────┘
❯ For TLSN_TRUSTED_NOTARY_KEY, how can this be at least onchain verifiable?
⏺ Great question. Right now the trusted notary key is a static config value with zero on-chain binding — you're trusting whoever
configured it. Let me walk through the options from simplest to most robust.
★ Insight ─────────────────────────────────────
The core trust question: who decides which notary keys are trustworthy, and how do verifiers (operators, challengers, contracts)
agree on that set? This is analogous to how OperatorRegistry solves "which operator BLS keys are trustworthy" — by putting the
source of truth on-chain.
─────────────────────────────────────────────────
Option 1: NotaryRegistry Contract (Most Natural Fit)
Follow the same pattern as OperatorRegistry and PolicyClientRegistry:
// contracts/src/core/NotaryRegistry.sol
contract NotaryRegistry is OwnableUpgradeable {
/// notary address => notary public key (ed25519, 32 bytes)
mapping(address => bytes32) public notaryKeys;
/// notary address => active status
mapping(address => bool) public isActiveNotary;
event NotaryRegistered(address indexed notary, bytes32 publicKey);
event NotaryDeactivated(address indexed notary);
function registerNotary(address notary, bytes32 publicKey) external onlyOwner {
notaryKeys[notary] = publicKey;
isActiveNotary[notary] = true;
emit NotaryRegistered(notary, publicKey);
}
function deactivateNotary(address notary) external onlyOwner {
isActiveNotary[notary] = false;
emit NotaryDeactivated(notary);
}
}
Then the verification path becomes:
flowchart LR
P[Presentation] -->|contains notary pubkey| V[Verifier]
V -->|check| R[NotaryRegistry.isActiveNotary]
R -->|key matches + active| OK[Accept]
R -->|mismatch or inactive| REJECT[Reject]
Operators read from the contract at startup (or cache with periodic refresh), replacing the env var entirely. The Rego extension
could also read it:
In newton.crypto.tlsn_verify():
1. Verify presentation signature
2. Extract notary pubkey from attestation
3. Check notary pubkey is registered on-chain via NotaryRegistry
Pros: Simple, follows existing Newton patterns, governance-controlled via contract owner (multisig).
Cons: Centralized trust (owner decides who's trusted), no economic incentive for notaries.
Option 2: Stake-Backed Notary Registration
Extend Option 1 with economic security:
contract StakedNotaryRegistry {
uint256 public constant MIN_NOTARY_STAKE = 1 ether;
uint256 public constant SLASH_AMOUNT = 0.1 ether; // 10%
struct Notary {
bytes32 publicKey;
uint256 stake;
bool active;
}
mapping(address => Notary) public notaries;
function registerNotary(bytes32 publicKey) external payable {
require(msg.value >= MIN_NOTARY_STAKE, "Insufficient stake");
notaries[msg.sender] = Notary(publicKey, msg.value, true);
}
/// Called when a notary signed a false attestation
function slashNotary(
address notary,
bytes calldata fraudProof
) external {
// Verify the fraud proof (e.g., the attested data contradicts
// what the TLS server actually served)
require(verifyFraudProof(notary, fraudProof), "Invalid fraud proof");
uint256 slashAmount = notaries[notary].stake * SLASH_AMOUNT / 1 ether;
notaries[notary].stake -= slashAmount;
// ... transfer slash to challenger
}
}
Pros: Economic security, permissionless notary registration, aligned incentives.
Cons: Complex fraud proof mechanism (how do you prove a notary lied about TLS data?), higher implementation cost.
Option 3: EigenLayer-Native (Notary as Operator Role)
Since Newton is already an EigenLayer AVS, the most elegant approach might be making the notary role an extension of the existing
operator set:
// Extend OperatorRegistry or add a role flag
function registerAsNotary(bytes32 notaryPublicKey) external {
require(isRegisteredOperator(msg.sender), "Must be registered operator");
operatorNotaryKeys[msg.sender] = notaryPublicKey;
emit NotaryRoleAdded(msg.sender, notaryPublicKey);
}
This means:
- Notaries are operators who already have restaked ETH via EigenLayer
- Slashing for false attestations uses the existing EigenLayer slashing mechanism
- No new staking infrastructure needed
- The quorumThresholdPercentage model already handles "how many notaries must agree"
Pros: Reuses EigenLayer security, no new contracts for staking/slashing, operators already have skin in the game.
Cons: Couples notary trust to operator trust (may want independent trust sets), requires operators to run notary infrastructure.
Option 4: On-Chain Key Commitment (Minimal)
The lightest-weight option — just commit the key hash on-chain without a full registry:
// In NewtonProverTaskManager or a dedicated contract
bytes32 public trustedNotaryKeyHash;
function setTrustedNotaryKeyHash(bytes32 keyHash) external onlyOwner {
trustedNotaryKeyHash = keyHash;
emit TrustedNotaryKeyUpdated(keyHash);
}
Operators verify locally that keccak256(notary_pubkey) == trustedNotaryKeyHash. This is the minimum viable on-chain binding — it
doesn't prevent a bad notary, but it ensures all operators agree on which key to trust, and key rotation is visible on-chain.
Recommendation
For Newton's current stage, I'd suggest Option 1 (NotaryRegistry) with a path to Option 3 (EigenLayer-native):
┌────────┬───────────────────────────────────────────────────────┬────────────┐
│ Phase │ Approach │ Complexity │
├────────┼───────────────────────────────────────────────────────┼────────────┤
│ Now │ NotaryRegistry contract, governance-controlled │ Low │
├────────┼───────────────────────────────────────────────────────┼────────────┤
│ Next │ Operators can opt-in as notaries (dual role) │ Medium │
├────────┼───────────────────────────────────────────────────────┼────────────┤
│ Future │ Full stake-backed notary set with EigenLayer slashing │ High │
└────────┴───────────────────────────────────────────────────────┴────────────┘
The immediate win is replacing the env var with an on-chain registry read. The Rego extension (newton.crypto.tlsn_verify()) would
then:
- Extract notary public key from the TLSNotary attestation
- Call NotaryRegistry.isActiveNotary(notaryAddress) + verify key match
- Only accept presentations signed by on-chain registered notaries
This makes the trust anchor transparent, auditable, and governable — instead of hidden in a TOML config that each operator sets
independently.
❯ 1. I don't understand this: [Pasted text #1 +7 lines] the desired design is that mpc tls proof is fetched and verified from
newton_crpto rego extension (lib/regorus) (similar to in Rego, we have newton_crypto extension
newton.crypto.ecdsa_recover_signerthat checks for ecdsa signature) for the redacted mpc-tls data could be verified/used inthe policy logic. why are there two different paths? What different use cases? 2.1 1) What is in-memory duplex? It says
"Production flow requires WebSocket (sidecar)". What is the limitation of in-memory duplex? 2) Verify against trusted notary
key --> this notary key should be in the config toml file for tls_notary.toml in @crates/tls-notary crate, following the
pattern of other binary crates like @crates/gateway/gateway.toml or @crates/operator/operator.toml , etc. 3) - Serialization:
Uses bincode 2.x serde API (bincode::serde::encode_to_vec / decode_from_slice) — but on the phase3 branch, the
README shows bincode::serialize() (bincode 1.x API). This is a version inconsistency between what's on main (bincode 2.x) and
what's documented on later branches. --> this should be fixed. Should use version 2.x 4) In-memory duplex: prove.rs uses
tokio::io::duplex(MAX_RECV_BUFFER) for prover ↔ verifier. This only works for testing. The
sidecar uses WebSocket for the real flow. --> this should definitely be fixed. (one question: if tls-notary rpc server is
"combined" with the existing gateway service, what are pros vs. cons? In this combined case, can we use in memory duplex in
production as well? Or should we keep the separation between gateway vs tls-notary server)? 2.2 1) All the security concerns
are valid and should be addressed in the PR #379 2) "ws_adapter.rs is a full reimplementation of axum's WebSocket." --> this
makes it look like the author clearly just blindly relied on ai for coding without the author having understanding of what is
happening. This is absolutely not acceptible. Clearly convey this message. 3) "[Pasted text #2 +4 lines]" this definitely needs
clarification/correction. Another example of author blindly relying on AI without knowing what is going on. 4) "8. Session
state is in-memory HashMap behind a Mutex. No persistence, no clustering. Sidecar must be single-instance per client.
Session reaper runs every 60 seconds." 5) "8. Session state is in-memory HashMap behind a Mutex. No persistence, no
clustering. Sidecar must be single-instance per client.
Session reaper runs every 60 seconds." --> hashmap behind a mutex usage concerns the performance/throughput/latency. Refer to
@crates/gateway/ where it uses Dashmap, RWLock, etc. depending on what is the optimal data structure for concurrent,
non-blocking, yet consistent enough storage data structure and evaluate if HashMap with Mutex is the right choice 6) " 9. IPFS
integration has no pinning. upload_to_ipfs calls /api/v0/add but doesn't pin. Garbage collection could remove proofs
before operators verify them." --> valid, this should be addressed. One question: why does the proofs need to be uploaded to
ipfs and later retrieved with cid? why can't the client/attestor just send along the proof directly gateway? is this just how
MPC-TLs work? 7) All Error Handling concerns are valid and should be addressed. AGAIN, another example of blindly trusting AI
(ex: data_provider/src/wasm/tlsn.rs uses bcs for deserialization while the rest of the codebase uses bincode.
verify_presentation_bytes calls bcs::from_bytes() — this is a serialization format mismatch. If the extension serializes with
bincode and the data provider deserializes with BCS, it will fail.) 8) ditto for "3. Rate limiter uses governor with DashMap.
Per-IP limiting is implemented. Global limiting is marked "reserved, not yet
enforced" in the config comments." --> Gateway already also implementes rate limiter already, should we consider combining
gateway and tls-notary (integrating tls-notary directly to gateway rpc server) 9) "14. No connection timeout on IPFS requests.
upload_to_ipfs and get_proof_handler use the shared reqwest::Client without explicit
timeouts on individual requests." --> valid. should be addressed. 2.3 All issues in section 2.3 are valid and should be
addressed for sure. 2.4 Similar to operator verifying tls proof is not per design, same for wasm has well. We just want to have
the custom newton rego extension as the single path for tls proof verification inside the extension logic, and give yes or no
for the Rego statement, which add up to policy evaluation as a whole. Lastly, yes, "BCS vs bincode inconsistency (critical).
tlsn.rs:119 uses bcs::from_bytes() but the core library serializes with
bincode::serde::encode_to_vec(). These are incompatible formats. This will cause deserialization failures at runtime." -->
this issue must be addressed. 2.5 "SendTaskRequest says "Not supported in async mode; returns an error if provided" --> this
seems incorrect. Send Task flow (async flow) should also support tls-notary proofs. just like create task flow. ======== above
are my high level review of the current state of tls notary project per newton protocol.