open-standards/joule-code/joule-code-rs/crates/joule-moq/examples/quic_xnet.rs
Transaction Science 7706471a82 joule-moq: add quic_xnet example — cross-machine receipt-verified delivery
A runnable publisher/subscriber over real QUIC, for proving the receipt-bearing
MoQT data path machine-to-machine (not just loopback). The publisher mints a
pinned self-signed cert, signs a group of objects, and streams the group receipt
plus each object with its inclusion proof; the subscriber dials over QUIC,
verifies the group signature against the pinned publisher key, and verifies each
object's payload + proof against the signed root.

Live-verified over a LAN: publisher on one Mac, subscriber on another, all three
objects verified end to end against the pinned key.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 84ec356153)
2026-06-27 07:12:43 -04:00

178 lines
7.6 KiB
Rust

//! Cross-machine MoQT-over-QUIC demo: a publisher streams signed,
//! receipt-bearing objects; a subscriber on another host verifies every object
//! against the publisher's pinned Ed25519 key over a real QUIC connection.
//!
//! Build: cargo build --example quic_xnet --features quic
//! Publish (host A):
//! quic_xnet publish 0.0.0.0:9443 /tmp/jmoq-cert.der /tmp/jmoq-pub.hex
//! Subscribe (host B), after copying the cert + pubkey files over:
//! quic_xnet subscribe 10.10.10.2:9443 /tmp/jmoq-cert.der <pubkey_hex>
//!
//! The TLS is a pinned self-signed cert (SAN `joule-pub`); the subscriber dials
//! with that server name and trusts only that cert, and trusts only the printed
//! publisher key as a receipt anchor.
#[cfg(not(feature = "quic"))]
fn main() {
eprintln!("rebuild with --features quic");
std::process::exit(2);
}
#[cfg(feature = "quic")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
rt.block_on(run())
}
#[cfg(feature = "quic")]
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
#[cfg(feature = "quic")]
fn hex_decode(s: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let s = s.trim();
if s.len() % 2 != 0 {
return Err("odd-length hex".into());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.into()))
.collect()
}
#[cfg(feature = "quic")]
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use bytes::Bytes;
use ed25519_dalek::{SigningKey, VerifyingKey};
use joule_moq::messages::ObjectHeader;
use joule_moq::receipt::{ObjectReceipt, ReceiptBuilder, ReceiptVerifier};
use joule_moq::track::{GroupId, ObjectStatus, TrackAlias};
use joule_moq::transport_quic::{
generate_self_signed, read_group_receipt_from_uni, read_object_from_uni,
write_group_receipt_to_uni, write_object_to_uni, QuicEndpoint,
};
use rand_core_06::OsRng;
use rustls::pki_types::CertificateDer;
let args: Vec<String> = std::env::args().collect();
let mode = args.get(1).map(|s| s.as_str()).unwrap_or("");
let track_alias = TrackAlias(7);
let group_id = GroupId(1);
let origin = "did:key:zJouleMoqPublisher";
match mode {
"publish" => {
let bind: std::net::SocketAddr = args.get(2).ok_or("need bind addr")?.parse()?;
let cert_out = args.get(3).ok_or("need cert out path")?;
let pubkey_out = args.get(4).ok_or("need pubkey out path")?;
let (chain, key) = generate_self_signed(vec!["joule-pub".into()])?;
std::fs::write(cert_out, chain[0].as_ref())?;
let signing_key = SigningKey::generate(&mut OsRng);
let pubkey_hex = hex_encode(&signing_key.verifying_key().to_bytes());
std::fs::write(pubkey_out, &pubkey_hex)?;
let server = QuicEndpoint::server(bind, chain, key)?;
println!("publisher: listening on {bind}");
println!("publisher: pubkey {pubkey_hex}");
println!("publisher: cert -> {cert_out}, pubkey -> {pubkey_out}");
let session = server.accept().await?;
println!("publisher: subscriber connected from {}", session.remote_address());
let frames = [
(Bytes::from_static(b"frame-one"), 100u64),
(Bytes::from_static(b"frame-two"), 110u64),
(Bytes::from_static(b"frame-three"), 120u64),
];
let mut builder = ReceiptBuilder::new(track_alias, group_id);
let mut pending = Vec::new();
for (payload, joules) in frames.iter() {
let hash = ObjectReceipt::hash_payload(payload);
let now = chrono::Utc::now().to_rfc3339();
let id = builder.add_object(hash, *joules, origin, now);
pending.push((id, payload.clone(), hash, *joules));
}
let proofs: Vec<_> = (0..pending.len()).map(|i| builder.proof_for(i).unwrap()).collect();
let group_end = chrono::Utc::now().to_rfc3339();
let group_receipt = builder.finalize(&signing_key, "joule-moq-xnet", group_end);
let mut uni = session.open_uni().await?;
write_group_receipt_to_uni(&mut uni, &group_receipt).await?;
for ((id, payload, hash, joules), proof) in pending.into_iter().zip(proofs) {
let header = ObjectHeader {
track_alias,
group_id,
object_id: id,
publisher_priority: 128,
status: ObjectStatus::Normal,
payload_len: payload.len() as u64,
mrl_layer: 0,
mrl_total_layers: 0,
wai_envelope: false,
};
let receipt = ObjectReceipt {
track_alias,
group_id,
object_id: id,
content_hash: hash,
joules_micro: joules,
origin: origin.to_string(),
merkle_proof: proof,
};
write_object_to_uni(&mut uni, &header, Some(&receipt), &payload).await?;
}
uni.finish()?;
println!("publisher: streamed group receipt + {} objects", group_receipt.leaf_count);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
println!("publisher: done");
Ok(())
}
"subscribe" => {
let connect: std::net::SocketAddr = args.get(2).ok_or("need connect addr")?.parse()?;
let cert_in = args.get(3).ok_or("need cert in path")?;
let pubkey_hex = args.get(4).ok_or("need pubkey hex")?;
let cert = CertificateDer::from(std::fs::read(cert_in)?);
let pubkey_bytes: [u8; 32] = hex_decode(pubkey_hex)?
.try_into()
.map_err(|_| "pubkey must be 32 bytes")?;
let publisher_key = VerifyingKey::from_bytes(&pubkey_bytes)?;
let client = QuicEndpoint::client("0.0.0.0:0".parse()?, vec![cert])?;
let session = client.connect(connect, "joule-pub").await?;
println!("subscriber: connected to {connect}");
let mut uni = session.accept_uni().await?;
let group_receipt = read_group_receipt_from_uni(&mut uni).await?;
let verifier = ReceiptVerifier::with_anchors(vec![publisher_key]);
let root = verifier.verify_group(&group_receipt)?;
println!(
"subscriber: group receipt verified vs pinned key — {} objects, {} µJ, root {}",
group_receipt.leaf_count,
group_receipt.total_joules_micro,
hex_encode(&root[..8]),
);
for i in 0..group_receipt.leaf_count {
let (_header, receipt, payload) = read_object_from_uni(&mut uni).await?;
let receipt = receipt.ok_or("object missing its receipt")?;
verifier.verify_payload(&payload, &receipt, &root)?;
println!(
"subscriber: object {i} VERIFIED — {} ({} µJ)",
String::from_utf8_lossy(&payload),
receipt.joules_micro,
);
}
println!("subscriber: ALL OBJECTS VERIFIED over real QUIC, cross-machine");
Ok(())
}
_ => {
eprintln!("usage: quic_xnet publish <bind> <cert_out> <pubkey_out>");
eprintln!(" quic_xnet subscribe <connect> <cert_in> <pubkey_hex>");
std::process::exit(2);
}
}
}