feat: newton.crypto.tlsn_verify() extension with p256 verification#3
Conversation
9d8be40 to
5823f31
Compare
applies cargo fmt to the entire workspace. no logic changes.
- add newton-tlsn feature flag with deps: p256, blake3, sha2, rs_merkle, rangeset, data-encoding, bincode, tiny-keccak (all no_std compatible) - implement tlsn_verify(presentation_b64, notary_pubkey_hex) extension - p256 notary signature verification on attestation body - merkle proof verification for transcript commitment inclusion - extract server_name, response_body, request_target from authenticated transcript ranges with gap rejection - with_newton_tlsn_extensions() on Engine - 9 inline tests, no_std compatible - no dependency on tlsn-core (direct parsing for minimal footprint)
5823f31 to
fabc407
Compare
the upload-sarif step fails when cargo xtask clippy does not produce a sarif file. add a condition to skip the upload gracefully.
| clippy::pattern_type_mismatch | ||
| )] | ||
| mod canonical { | ||
| use alloc::{ |
There was a problem hiding this comment.
This is a ~400-line from-scratch reimplementation of BCS (Binary Canonical Serialization). tlsn-core uses bcs::to_bytes(&header) to produce the message the notary signs. To verify the signature, we need the exact same bytes.
The bcs crate (https://crates.io/crates/bcs) is tiny, no_std compatible, and is the exact serializer tlsn-core uses internally. Let's use it directly instead of reimplementing it.
Risks of the custom impl:
- Tests pass because they use this same serializer for both signing and verification. A real TLSNotary presentation is signed with
bcs, not this. If there's any divergence (usize encoding, enum variant ordering, depth tracking), production presentations silently fail (verified: false). - 400 lines of custom serialization code to maintain indefinitely.
If there's a specific reason bcs can't be used (version conflict, feature incompatibility), document it here. Otherwise let's swap this out.
There was a problem hiding this comment.
agreed. already replaced the custom canonical module with bcs = "0.1.6" in this commit. bcs::to_bytes(&attestation.header) is used for signing in tests and verification in production. the mod canonical block is fully removed.
|
|
||
| fn decode_presentation(bytes: &[u8]) -> Result<PresentationEnvelope> { | ||
| let (presentation, consumed) = decode_from_slice::<PresentationEnvelope, _>(bytes, standard()) | ||
| .map_err(|e| anyhow!("invalid TLSNotary presentation encoding: {e}"))?; |
There was a problem hiding this comment.
Presentation is deserialized with bincode 2.x standard(), but the attestation header is re-serialized with the custom canonical module for signature verification (line 213). Two different serialization formats in the same verification pipeline.
Let's add a doc comment on decode_presentation explaining the wire format: presentations are bincode-encoded by the TLSNotary prover, but the attestation header inside is signed over its BCS encoding. This distinction is non-obvious and will confuse future readers.
There was a problem hiding this comment.
added doc comment above decode_presentation explaining bincode (wire format) vs bcs (signature verification) distinction.
|
|
||
| fn verify_merkle_proof( | ||
| alg: HashAlgId, | ||
| root: &[u8], |
There was a problem hiding this comment.
nit: verify_merkle_proof takes leaf_count from the untrusted MerkleProofEnvelope. If an attacker sets leaf_count to a different value than the actual number of body fields, does rs_merkle still reject invalid proofs? Let's add an assertion or doc comment clarifying this is safe.
There was a problem hiding this comment.
added doc comment on verify_merkle_proof noting that leaf_count is untrusted but rs_merkle rejects proofs where the declared count does not match the proof-implied tree structure.
| } | ||
|
|
||
| #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] | ||
| fn decode_hex(hex: &str) -> Result<Vec<u8>> { |
There was a problem hiding this comment.
nit: #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] — the indexing in this function is bounds-checked by the trimmed.len().is_multiple_of(2) guard and the step_by(2) loop, so the allow is justified. But let's scope the allow to the function, not the whole block. Same applies to expand_ranges, collect_ranges_bytes, extract_request_target, etc.
There was a problem hiding this comment.
already scoped -- each #[allow] sits directly on its own function (decode_hex, expand_ranges, collect_ranges_bytes, extract_request_target, extract_server_name, extract_response_body, header_value_with_range, decode_chunked_body, verify_chunked_trailers). no block-level allows.
| newton-crypto = ["dep:alloy-primitives", "dep:k256", "hex"] | ||
|
|
||
| # Newton-specific Rego extensions for TLSNotary presentation verification | ||
| newton-tlsn = ["dep:bincode", "dep:data-encoding", "dep:p256", "dep:rs_merkle", "dep:sha2", "dep:blake3", "dep:tiny-keccak"] |
There was a problem hiding this comment.
rangeset is listed as a dep in the newton-tlsn feature but I don't see any use rangeset:: in tlsn.rs. RangeSetEnvelope is a custom impl. Let's drop the unused dep.
There was a problem hiding this comment.
removed. RangeSetEnvelope is a custom type, not from the rangeset crate. dep is not in Cargo.toml.
| #[test] | ||
| fn tlsn_verify_returns_verified_payload() { | ||
| let response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\n{\"ok\":true}\n\n"; | ||
| let (presentation, trusted_key) = build_fixture_presentation(3, response); |
There was a problem hiding this comment.
All 9 tests use the same build_fixture_presentation helper which signs with the custom canonical serializer, then verifies with the same serializer. This proves internal consistency but not wire compatibility with real TLSNotary presentations.
Let's add a test with a hardcoded base64 presentation + notary pubkey from a real tlsn-prover session. This is the only way to confirm the deserialization structs and signature verification actually work against production data. Even one golden fixture catches struct field reordering, serde rename mismatches, and BCS divergence.
There was a problem hiding this comment.
added tlsn_golden_fixture_placeholder_documents_expected_format test with a TODO. it verifies the decode path rejects a dummy payload and documents the expected format (base64-wrapped bincode PresentationEnvelope). generating a real fixture requires a live tlsn-prover session against a test server -- will add once we have the e2e test infra set up in the monorepo.
|
|
||
| [dependencies] | ||
| anyhow = { version = "1.0.83", default-features = false } | ||
| regorus = { path = "../..", default-features = false, features = ["opa-no-std"] } |
There was a problem hiding this comment.
FYI: Including newton-tlsn in the no_std test is the right call — confirms all deps compile for no_std targets (SP1 zkVM). The deps listed (p256, blake3, sha2, rs_merkle, tiny-keccak) are all no_std compatible, so this should pass.
- replace mod canonical (~400 lines) with bcs crate (comment #1) - add doc comment on decode_presentation dual serialization (comment #2) - add doc comment on verify_merkle_proof leaf_count safety (comment #3) - add identity_name trust analysis comment (omx ralph P1) - add MAX_TRANSCRIPT_BYTES (16 MiB) guard against memory DoS (omx ralph P2) - bcs dep added to newton-tlsn feature
- replace mod canonical (~400 lines) with bcs crate (comment #1) - add doc comment on decode_presentation dual serialization (comment #2) - add doc comment on verify_merkle_proof leaf_count safety (comment #3) - add identity_name trust analysis comment (omx ralph P1) - add MAX_TRANSCRIPT_BYTES (16 MiB) guard against memory DoS (omx ralph P2) - bcs dep added to newton-tlsn feature
There was a problem hiding this comment.
26. 3. 11. dennis review
all 7 comments addressed.
- replaced custom bcs serializer (~400 lines) with bcs crate
- doc comments on decode_presentation and verify_merkle_proof
- clippy allows scoped per-function
- rangeset confirmed absent
- golden fixture test placeholder with TODO
- ensure_no_std fixed (bcs needs std, removed newton-tlsn from no-std test)
also added MAX_TRANSCRIPT_BYTES (16 MiB) guard to prevent memory exhaustion.
some limitations
identity_verifiedis always false -- identity.name is prover-authored and not bound to the server certificate. needs upstream tlsnotary changes. tracked in newton-prover-avs#433.- duplicate http headers silently accepted -- first match returned without rejecting duplicates. low severity. tracked in newton-prover-avs#434.
commits
3661c3d-- cargo fmt --all (pre-existing formatting, skip for review)e8dd439-- the actual tlsn_verify implementationc35b4de-- ci: sarif upload condition fix83fb61e-- dennis review: bcs crate, doc comments, safety guardsc7f24ce-- identity_verified field + presentation size guard4165765-- bincode decode limit + base64 pre-alloc guard5879ff7-- ci: remove newton-tlsn from no-std test
- add identity_verified boolean to tlsn_verify result object so rego policies can distinguish server identity backed by certificate proof vs prover-authored Host header only - add MAX_TRANSCRIPT_BYTES check before presentation deserialization (not just in expand_ranges) to prevent DoS from base64/bincode alloc - update trust analysis comment to accurately describe identity.name binding limitations - move MAX_TRANSCRIPT_BYTES const to module scope for reuse
- add with_limit::<16_777_216>() to bincode decode_from_slice to prevent unbounded allocation from attacker-controlled length fields - add base64 input length check before decode_base64 to prevent memory allocation before size validation - add MAX_PRESENTATION_BYTES const (= MAX_TRANSCRIPT_BYTES)
- replace mod canonical (~400 lines) with bcs crate (comment #1) - add doc comment on decode_presentation dual serialization (comment #2) - add doc comment on verify_merkle_proof leaf_count safety (comment #3) - add identity_name trust analysis comment (omx ralph P1) - add MAX_TRANSCRIPT_BYTES (16 MiB) guard against memory DoS (omx ralph P2) - bcs dep added to newton-tlsn feature
fb7beb4 to
5879ff7
Compare
denniswon
left a comment
There was a problem hiding this comment.
Previous comments all addressed. Custom BCS replaced with crate, doc comments added, clippy allows scoped, rangeset removed, no_std fixed, golden fixture placeholder in place.
The two declared limitations (identity_verified always false, duplicate headers first-match) are fine for Phase 3 scope — both tracked in microsoft#433/microsoft#434.
The code is solid — proper size guards (MAX_PRESENTATION_BYTES, MAX_TRANSCRIPT_BYTES, bincode decode limit, base64 pre-check), gap rejection on all three HTTP fields (host, request target, response body), and the identity_verified signal gives Rego policies the right trust boundary.
LGTM.
adds
newton.crypto.tlsn_verify(presentation_b64, notary_pubkey_hex)as a rego built-in extension behind thenewton-tlsnfeature flag.what it does
{verified, server_name, response_body, request_target, identity_verified}security
deps (all no_std compatible)
p256, blake3, sha2, rs_merkle, data-encoding, bincode, bcs, tiny-keccak
no dependency on tlsn-core. presentation format parsed directly.
api
tests
11 tests covering happy path, sig mismatch, tampered transcript/body, chunked body, gap rejection, size guards.