feat: added plugin-core crate

This commit is contained in:
dan
2025-10-24 10:41:45 +03:00
parent 5fef2af698
commit 0f9c04fc11
10 changed files with 900 additions and 189 deletions

375
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ members = [
"crates/data-fixtures",
"crates/examples",
"crates/formats",
"crates/plugin-core",
"crates/server-fixture/certs",
"crates/server-fixture/server",
"crates/tls/backend",
@@ -53,6 +54,7 @@ tlsn-formats = { path = "crates/formats" }
tlsn-hmac-sha256 = { path = "crates/components/hmac-sha256" }
tlsn-key-exchange = { path = "crates/components/key-exchange" }
tlsn-mpc-tls = { path = "crates/mpc-tls" }
tlsn-plugin-core = { path = "crates/plugin-core" }
tlsn-server-fixture = { path = "crates/server-fixture/server" }
tlsn-server-fixture-certs = { path = "crates/server-fixture/certs" }
tlsn-tls-backend = { path = "crates/tls/backend" }
@@ -83,9 +85,10 @@ mpz-ideal-vm = { git = "https://github.com/privacy-ethereum/mpz", tag = "v0.1.0-
rangeset = { version = "0.2" }
serio = { version = "0.2" }
spansy = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6168663" }
spansy = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "304b910" }
uid-mux = { version = "0.2" }
websocket-relay = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6168663" }
websocket-relay = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "304b910" }
futures-plex = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "304b910" }
aead = { version = "0.4" }
aes = { version = "0.8" }

View File

@@ -7,6 +7,7 @@ use crate::{
transcript::{Direction, Transcript},
webpki::CertificateDer,
};
use serde::{Deserialize, Serialize};
use tls_core::msgs::{
alert::AlertMessagePayload,
codec::{Codec, Reader},
@@ -15,7 +16,7 @@ use tls_core::msgs::{
};
/// A transcript of TLS records sent and received by the prover.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsTranscript {
time: u64,
version: TlsVersion,
@@ -291,7 +292,7 @@ impl TlsTranscript {
}
/// A TLS record.
#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub struct Record {
/// Sequence number.
pub seq: u64,

View File

@@ -3,4 +3,4 @@ Cookie: very-secret-cookie
Content-Length: 44
Content-Type: application/json
{"foo": "bar", "bazz": 123, "buzz": [1,"5"]}
{"foo": "bar", "bazz": 123, "buzz": [1,"5"]}

View File

@@ -0,0 +1,22 @@
[package]
name = "tlsn-plugin-core"
version = "0.1.0"
edition = "2024"
[dependencies]
tlsn = { workspace = true }
tlsn-core = { workspace = true }
tlsn-formats = { workspace = true }
http-body-util = { workspace = true }
hyper = { workspace = true, features = ["client", "http1"] }
rangeset = { workspace = true }
serde = { workspace = true }
spansy = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
tlsn-data-fixtures = { workspace = true }
[lints]
workspace = true

View File

@@ -0,0 +1,105 @@
//! Core types of the prover and verifier plugin.
use serde::{Deserialize, Serialize};
use tlsn_core::{
hash::HashAlgId,
transcript::{Direction, TranscriptCommitmentKind},
};
mod prover;
mod verifier;
pub use prover::{
Config as ProverPluginConfig, ConfigError as ProverPLuginConfigError,
Output as ProverPluginOutput,
};
pub use verifier::{
Config as VerifierPluginConfig, ConfigError as VerifierPluginConfigError,
Output as VerifierPluginOutput,
};
/// A rule for disclosing HTTP data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisclosureRule {
http: HttpHandle,
policy: DisclosurePolicy,
}
/// Handle for a part of an HTTP message.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpHandle {
typ: MessageType,
part: MessagePart,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum MessageType {
Request,
Response,
}
impl From<&MessageType> for Direction {
fn from(mt: &MessageType) -> Self {
match mt {
MessageType::Request => Direction::Sent,
MessageType::Response => Direction::Received,
}
}
}
/// Disclosure policy.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum DisclosurePolicy {
/// Reveals data.
Reveal,
/// Creates a hiding commitment.
Commit(Alg),
}
/// Commitment algorithm.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum Alg {
EncodingSha256,
EncodingBlake3,
EncodingKeccak256,
Sha256,
Blake3,
}
impl From<&Alg> for TranscriptCommitmentKind {
fn from(alg: &Alg) -> Self {
match alg {
Alg::EncodingSha256 | Alg::EncodingBlake3 | Alg::EncodingKeccak256 => {
TranscriptCommitmentKind::Encoding
}
Alg::Sha256 => TranscriptCommitmentKind::Hash {
alg: HashAlgId::SHA256,
},
Alg::Blake3 => TranscriptCommitmentKind::Hash {
alg: HashAlgId::BLAKE3,
},
}
}
}
/// The part of an HTTP message.
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum MessagePart {
All,
StartLine,
Header(HeaderParams),
Body(BodyParams),
}
/// Parameters for an HTTP header.
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct HeaderParams {
pub key: String,
}
/// Parameters for a part of an HTTP body.
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum BodyParams {
JsonPath(String),
XPath(String),
}

View File

@@ -0,0 +1,34 @@
//! Core types of the prover plugin.
use crate::HttpHandle;
use serde::{Deserialize, Serialize};
use tlsn_core::ProverOutput;
mod config;
pub use config::{Config, ConfigError};
/// Output of the prover plugin.
#[allow(dead_code)]
pub struct Output {
output: ProverOutput,
/// Plaintext exposed to the host.
plaintext: Vec<(HttpHandle, Vec<u8>)>,
}
/// Params for protocol prover.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProverParams {
max_recv_data: usize,
max_sent_data: usize,
prove_server_identity: bool,
pub server_dns: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpRequest {
url: String,
method: String,
body: Option<Vec<u8>>,
pub headers: Vec<(String, String)>,
}

View File

@@ -0,0 +1,463 @@
use crate::{
BodyParams, DisclosurePolicy, DisclosureRule, HttpHandle, MessagePart, MessageType,
prover::{HttpRequest, ProverParams},
};
use crate::prover::Output;
use http_body_util::Full;
use hyper::{Request as HyperRequest, body::Bytes};
use rangeset::RangeSet;
use serde::{Deserialize, Serialize};
use tlsn::{
config::ProtocolConfig,
prover::{ProverConfig, TlsConfig},
};
use tlsn_core::{
ProveConfig, ProveConfigBuilder, ProverOutput,
connection::{DnsName, ServerName},
transcript::{Transcript, TranscriptCommitConfig, TranscriptCommitConfigBuilder},
webpki::RootCertStore,
};
use tlsn_formats::{
http::{Body, Request, Requests, Response, Responses},
json::JsonValue,
spansy,
spansy::Spanned,
};
/// Prover plugin config.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub prover_params: ProverParams,
pub request: HttpRequest,
/// Data which will be disclosed to the verifier.
pub disclose: Vec<DisclosureRule>,
/// Data which will be exposed in the plugin output.
pub expose: Vec<HttpHandle>,
pub root_store: RootCertStore,
pub verifier_endpoint: String,
/// Proxy endpoint for connecting to the server.
pub proxy_endpoint: Option<String>,
}
impl Config {
/// Returns the verifier endpoint.
pub fn prover_endpoint(&self) -> &String {
&self.verifier_endpoint
}
/// Builds and returns [ProverConfig].
pub fn prover_config(&self) -> Result<ProverConfig, ConfigError> {
let dns_name: DnsName = self
.prover_params
.server_dns
.clone()
.try_into()
.map_err(|_| ConfigError("prover_config error".to_string()))?;
let mut builder = TlsConfig::builder();
builder.root_store(self.root_store.clone());
let tls_config = builder.build().unwrap();
let config = ProverConfig::builder()
.server_name(ServerName::Dns(dns_name))
.tls_config(tls_config)
.protocol_config(
ProtocolConfig::builder()
.max_sent_data(self.prover_params.max_sent_data)
.max_recv_data(self.prover_params.max_recv_data)
.build()
.unwrap(),
)
.build()
.unwrap();
Ok(config)
}
/// Returns the HTTP request.
pub fn http_request(&self) -> Result<HyperRequest<Full<Bytes>>, ConfigError> {
let mut request = HyperRequest::builder()
.uri(self.request.url.clone())
.header("Host", self.prover_params.server_dns.clone());
for (k, v) in &self.request.headers {
request = request.header(k, v);
}
request = request.method(self.request.method.as_str());
let body = match &self.request.body {
Some(data) => Full::<Bytes>::from(data.clone()),
None => Full::<Bytes>::from(vec![]),
};
request
.body(body)
.map_err(|_| ConfigError("http_request error".to_string()))
}
/// Creates a [ProveConfig] for the given `transcript`.
pub fn prove_config(&self, transcript: &Transcript) -> Result<ProveConfig, ConfigError> {
let mut prove_cfg = ProveConfig::builder(transcript);
let mut commit_cfg = TranscriptCommitConfig::builder(transcript);
if self.prover_params.prove_server_identity {
prove_cfg.server_identity();
}
let reqs = Requests::new_from_slice(transcript.sent())
.collect::<Result<Vec<_>, _>>()
.map_err(|_| ConfigError("prove_config error".to_string()))?;
let resps = Responses::new_from_slice(transcript.received())
.collect::<Result<Vec<_>, _>>()
.map_err(|_| ConfigError("prove_config error".to_string()))?;
let req = reqs.first().expect("at least one request");
let resp = resps.first().expect("at least one response");
let req_rules = self
.disclose
.iter()
.filter(|h| h.http.typ == MessageType::Request);
let resp_rules = self
.disclose
.iter()
.filter(|h| h.http.typ == MessageType::Response);
disclose_req(req, req_rules, &mut commit_cfg, &mut prove_cfg);
disclose_resp(resp, resp_rules, &mut commit_cfg, &mut prove_cfg);
prove_cfg.transcript_commit(commit_cfg.build().unwrap());
Ok(prove_cfg.build().unwrap())
}
/// Returns the output of the plugin.
pub fn output(
&self,
transcript: Transcript,
prover_output: ProverOutput,
) -> Result<Output, ConfigError> {
let reqs = Requests::new_from_slice(transcript.sent())
.collect::<Result<Vec<_>, _>>()
.map_err(|_| ConfigError("output error".to_string()))?;
let resps = Responses::new_from_slice(transcript.received())
.collect::<Result<Vec<_>, _>>()
.map_err(|_| ConfigError("output error".to_string()))?;
let req = reqs.first().expect("at least one request");
let resp = resps.first().expect("at least one response");
let mut exposed = Vec::new();
// Extract the to-be-exposed data from the transcript.
for h in self.expose.iter() {
let range = if h.typ == MessageType::Request {
req_part_range(req, h)
} else {
resp_part_range(resp, h)
};
let seq = transcript
.get((&h.typ).into(), &range)
.ok_or(ConfigError("range not found in transcript".to_string()))?;
exposed.push((h.clone(), seq.data().to_vec()));
}
Ok(Output {
output: prover_output,
plaintext: exposed,
})
}
}
#[derive(Debug, thiserror::Error)]
#[error("config error: {0}")]
pub struct ConfigError(String);
/// Processes disclosure rules for the request.
fn disclose_req<'a, I>(
req: &Request,
rules: I,
commit_cfg: &mut TranscriptCommitConfigBuilder<'_>,
prove_cfg: &mut ProveConfigBuilder<'_>,
) where
I: Iterator<Item = &'a DisclosureRule>,
{
for r in rules {
let range = req_part_range(req, &r.http);
if range.is_empty() {
// TODO: maybe return an error here when the part was not found.
return;
}
match &r.policy {
DisclosurePolicy::Commit(alg) => {
commit_cfg
.commit_with_kind(&range, (&r.http.typ).into(), alg.into())
.expect("range is in the transcript");
}
DisclosurePolicy::Reveal => {
prove_cfg
.reveal_sent(&range)
.expect("range is in the transcript");
}
}
}
}
/// Processes disclosure rules for the response.
fn disclose_resp<'a, I>(
resp: &Response,
rules: I,
commit_cfg: &mut TranscriptCommitConfigBuilder<'_>,
prove_cfg: &mut ProveConfigBuilder<'_>,
) where
I: Iterator<Item = &'a DisclosureRule>,
{
for r in rules {
let range = resp_part_range(resp, &r.http);
if range.is_empty() {
// TODO: maybe return an error here when the part was not found.
return;
}
match &r.policy {
DisclosurePolicy::Commit(alg) => {
commit_cfg
.commit_with_kind(&range, (&r.http.typ).into(), alg.into())
.expect("range is in the transcript");
}
DisclosurePolicy::Reveal => {
prove_cfg
.reveal_recv(&range)
.expect("range is in the transcript");
}
}
}
}
/// Returns the range for the given `part` of the HTTP request,
fn req_part_range(req: &Request, part: &HttpHandle) -> RangeSet<usize> {
match &part.part {
MessagePart::All => {
(req.span().indices().min().unwrap()..req.span().indices().end().unwrap()).into()
}
MessagePart::StartLine => req.request.span().indices().clone(),
MessagePart::Header(params) => req
.headers_with_name(params.key.as_str())
.map(|h| h.span().indices())
.fold(RangeSet::default(), |acc, r| acc | r),
MessagePart::Body(params) => match &req.body {
Some(body) => {
// Body offset from the start of an HTTP message.
let body_offset = body
.span()
.indices()
.min()
.expect("body span cannot be empty");
let mut range = body_params_range(body, params);
range.shift_right(&body_offset);
range
}
None => RangeSet::default(),
},
}
}
/// Returns the range for the given `part` of the HTTP response,
fn resp_part_range(resp: &Response, part: &HttpHandle) -> RangeSet<usize> {
match &part.part {
MessagePart::All => {
(resp.span().indices().min().unwrap()..resp.span().indices().end().unwrap()).into()
}
MessagePart::StartLine => resp.status.span().indices().clone(),
MessagePart::Header(params) => resp
.headers_with_name(params.key.as_str())
.map(|h| h.span().indices())
.fold(RangeSet::default(), |acc, r| acc | r),
MessagePart::Body(params) => match &resp.body {
Some(body) => {
// Body offset from the start of an HTTP message.
let body_offset = body.span().indices().min().expect("body cannot be empty");
let mut range = body_params_range(body, params);
range.shift_right(&body_offset);
range
}
None => RangeSet::default(),
},
}
}
/// Returns the byte range of the `params` in the given `body`.
fn body_params_range(body: &Body, params: &BodyParams) -> RangeSet<usize> {
match params {
BodyParams::JsonPath(path) => {
// TODO: use a better approach than re-parsing the entire
// json for each path.
match spansy::json::parse(body.as_bytes().to_vec().into()) {
Ok(json) => json_path_range(&json, path),
Err(_) => RangeSet::default(),
}
}
_ => unimplemented!("only json parsing is currently supported"),
}
}
/// Returns the byte range of the keyvalue pair corresponding to the given
/// `path` in a JSON value `source`.
///
/// If the path points to an array element, only the range of the **value**
/// of the element is returned.
fn json_path_range(source: &JsonValue, path: &String) -> RangeSet<usize> {
let val = match source.get(path) {
Some(val) => val,
None => return RangeSet::default(),
};
let dot = ".";
let last = path.split(dot).last().unwrap();
// Whether `path` is a top-level key.
let is_top_level = last == path;
if last.parse::<usize>().is_ok() {
// The path points to an array element, so we only need the range of
// the **value**.
val.span().indices().clone()
} else {
let parent_val = if is_top_level {
source
} else {
source
.get(&path[..path.len() - last.len() - dot.len()])
.expect("path is valid")
};
let JsonValue::Object(parent_obj) = parent_val else {
unreachable!("parent value is always an object");
};
// We need the range of the **key-value** pair.
let kv = parent_obj
.elems
.iter()
.find(|kv| kv.value == *val)
.expect("element exists");
kv.without_separator()
}
}
#[cfg(test)]
mod tests {
use crate::HeaderParams;
use super::*;
use spansy::http::parse_response;
use tlsn_data_fixtures::http::{request, response};
use tlsn_formats::spansy::http::parse_request;
#[test]
fn test_req_part_range() {
let data = request::POST_JSON;
let req = parse_request(data).unwrap();
let s = std::str::from_utf8(data).unwrap();
//===============All
let part = HttpHandle {
part: MessagePart::All,
typ: MessageType::Request,
};
let range = req_part_range(&req, &part);
assert_eq!(range, 0..data.len());
//===============StartLine
let part = HttpHandle {
part: MessagePart::StartLine,
typ: MessageType::Request,
};
let range = req_part_range(&req, &part);
let end = s.find("\r\n").unwrap() + 2;
assert_eq!(range, 0..end);
//===============Header
let part = HttpHandle {
part: MessagePart::Header(HeaderParams {
key: "Content-Length".to_string(),
}),
typ: MessageType::Request,
};
let range = req_part_range(&req, &part);
let target: &'static str = "Content-Length: 44";
let start = s.find(target).unwrap();
let end = start + target.len() + 2;
assert_eq!(range, start..end);
//===============Body
let part = HttpHandle {
part: MessagePart::Body(BodyParams::JsonPath("bazz".to_string())),
typ: MessageType::Request,
};
let range = req_part_range(&req, &part);
let target: &'static str = "\"bazz\": 123";
let start = s.find(target).unwrap();
let end = start + target.len();
assert_eq!(range, start..end);
}
#[test]
fn test_resp_part_range() {
let data = response::OK_JSON;
let resp = parse_response(data).unwrap();
let s = std::str::from_utf8(data).unwrap();
//===============All
let part = HttpHandle {
part: MessagePart::All,
typ: MessageType::Response,
};
let range = resp_part_range(&resp, &part);
assert_eq!(range, 0..data.len());
//===============StartLine
let part = HttpHandle {
part: MessagePart::StartLine,
typ: MessageType::Response,
};
let range = resp_part_range(&resp, &part);
let end = s.find("\r\n").unwrap() + 2;
assert_eq!(range, 0..end);
//===============Header
let part = HttpHandle {
part: MessagePart::Header(HeaderParams {
key: "Content-Length".to_string(),
}),
typ: MessageType::Response,
};
let range = resp_part_range(&resp, &part);
let target: &'static str = "Content-Length: 44";
let start = s.find(target).unwrap();
let end = start + target.len() + 2;
assert_eq!(range, start..end);
//===============Body
let part = HttpHandle {
part: MessagePart::Body(BodyParams::JsonPath("bazz".to_string())),
typ: MessageType::Request,
};
let range = resp_part_range(&resp, &part);
let target: &'static str = "\"bazz\": 123";
let start = s.find(target).unwrap();
let end = start + target.len();
assert_eq!(range, start..end);
}
}

View File

@@ -0,0 +1,20 @@
//! Core types of the verifier plugin.
use tlsn_core::VerifierOutput;
mod config;
pub use config::{Config, ConfigError};
/// Output of the verifier plugin.
#[allow(dead_code)]
pub struct Output {
output: VerifierOutput,
}
/// Params for protocol verifier.
pub struct VerifierParams {
pub max_sent_data: usize,
pub max_recv_data: usize,
pub prover_endpoint: String,
}

View File

@@ -0,0 +1,56 @@
use crate::{
DisclosureRule,
verifier::{Output, VerifierParams},
};
use tlsn::{
config::{ProtocolConfig, RootCertStore},
verifier::VerifierConfig,
};
use tlsn_core::VerifierOutput;
/// Verifier plugin config.
#[allow(dead_code)]
pub struct Config {
pub verifier_params: VerifierParams,
/// Data which the prover is expected to disclose.
pub disclose: Vec<DisclosureRule>,
pub root_store: RootCertStore,
pub prover_endpoint: String,
}
impl Config {
/// Returns the prover endpoint.
pub fn prover_endpoint(&self) -> &String {
&self.verifier_params.prover_endpoint
}
/// Builds and returns [VerifierConfig].
pub fn verifier_config(&self) -> VerifierConfig {
VerifierConfig::builder()
.root_store(self.root_store.clone())
.build()
.unwrap()
}
/// Validates the given protocol `config`.
pub fn validate_protocol_config(&self, config: &ProtocolConfig) -> Result<(), ConfigError> {
if config.max_recv_data() > self.verifier_params.max_recv_data
|| config.max_sent_data() > self.verifier_params.max_sent_data
{
Err(ConfigError(
"failed to validate protocol config".to_string(),
))
} else {
Ok(())
}
}
/// Returns verifier plugin output.
pub fn output(&self, output: VerifierOutput) -> Output {
Output { output }
}
}
#[derive(Debug, thiserror::Error)]
#[error("config error: {0}")]
pub struct ConfigError(String);