lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 64985502122dcff4022eeb6681622d9aad23e2e2
parent bad7c1874598a6f8f4f2234779f0d5391eb08ca1
Author: triesap <tyson@radroots.org>
Date:   Wed, 20 May 2026 02:02:52 +0000

trade: harden validation receipt proof material

- add canonical base64 validation for inline SP1 proof bytes
- require sha256 proof-reference identifiers for referenced SP1 material
- wire base64 as an alloc-only workspace dependency for trade receipts
- cover invalid proof material and reference grammar in receipt tests

Diffstat:
MCargo.lock | 1+
MCargo.toml | 2+-
Mcrates/trade/Cargo.toml | 2++
Mcrates/trade/src/validation_receipt.rs | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
4 files changed, 76 insertions(+), 31 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4453,6 +4453,7 @@ version = "0.1.0-alpha.2" name = "radroots_trade" version = "0.1.0-alpha.2" dependencies = [ + "base64 0.22.1", "hex", "radroots_core", "radroots_events", diff --git a/Cargo.toml b/Cargo.toml @@ -101,7 +101,7 @@ radroots_sp1_guest_trade = { path = "crates/sp1_guest_trade", version = "0.1.0-a radroots_sp1_host_trade = { path = "crates/sp1_host_trade", version = "0.1.0-alpha.2", default-features = false } anyhow = { version = "1" } -base64 = { version = "0.22" } +base64 = { version = "0.22", default-features = false, features = ["alloc"] } bincode = { version = "1.3.3" } chacha20poly1305 = { version = "0.10.1", default-features = false, features = [ "alloc", diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml @@ -24,6 +24,7 @@ serde = [ ] serde_json = [ "serde", + "dep:base64", "dep:hex", "dep:serde_json", "dep:sha2", @@ -35,6 +36,7 @@ ts-rs = ["dep:ts-rs", "radroots_events/ts-rs", "radroots_events/std"] radroots_core = { workspace = true, default-features = false } radroots_events = { workspace = true, default-features = false } radroots_events_codec = { workspace = true, default-features = false } +base64 = { workspace = true, optional = true } hex = { workspace = true, optional = true } serde = { workspace = true, default-features = false, features = [ "alloc", diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs @@ -7,6 +7,7 @@ use alloc::{ vec::Vec, }; +use base64::Engine as _; use radroots_events::{ RadrootsNostrEvent, kinds::{KIND_TRADE_RECEIPT, KIND_TRADE_VALIDATION_RECEIPT}, @@ -21,6 +22,7 @@ pub const VALIDATION_RECEIPT_DOMAIN: &str = "radroots.receipt"; pub const VALIDATION_RECEIPT_VERSION: u32 = 1; pub const VALIDATION_RECEIPT_PUBLIC_VALUES_HASH_DOMAIN: &[u8] = b"radroots:sp1-public-values:v1"; pub const VALIDATION_RECEIPT_PROOF_REFERENCE_SCHEME: &str = "radroots-proof://"; +pub const VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX: &str = "radroots-proof://sha256/"; pub const TAG_VALIDATION_RECEIPT_EVENT_SET_ROOT: &str = "event_set_root"; pub const TAG_VALIDATION_RECEIPT_PROOF_SYSTEM: &str = "proof_system"; pub const TAG_VALIDATION_RECEIPT_PUBLIC_VALUES_HASH: &str = "public_values_hash"; @@ -587,47 +589,33 @@ fn validate_required_str( fn validate_inline_proof_base64(value: &str) -> Result<(), RadrootsValidationReceiptError> { validate_required_str(value, "proof.inline_proof_base64")?; - if value.len() % 4 != 0 { + let decoded = base64::engine::general_purpose::STANDARD + .decode(value) + .map_err(|_| { + RadrootsValidationReceiptError::InvalidProofMetadata("proof.inline_proof_base64") + })?; + if decoded.is_empty() || base64::engine::general_purpose::STANDARD.encode(&decoded) != value { return Err(RadrootsValidationReceiptError::InvalidProofMetadata( "proof.inline_proof_base64", )); } - let bytes = value.as_bytes(); - let mut padding_started = false; - let mut padding_count = 0usize; - for (index, byte) in bytes.iter().copied().enumerate() { - match byte { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' if !padding_started => {} - b'=' => { - padding_started = true; - padding_count += 1; - if padding_count > 2 || index < bytes.len().saturating_sub(2) { - return Err(RadrootsValidationReceiptError::InvalidProofMetadata( - "proof.inline_proof_base64", - )); - } - } - _ => { - return Err(RadrootsValidationReceiptError::InvalidProofMetadata( - "proof.inline_proof_base64", - )); - } - } - } - Ok(()) } fn validate_proof_reference(value: &str) -> Result<(), RadrootsValidationReceiptError> { validate_required_str(value, "proof.proof_reference")?; - let body = value - .strip_prefix(VALIDATION_RECEIPT_PROOF_REFERENCE_SCHEME) + let digest = value + .strip_prefix(VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX) .ok_or(RadrootsValidationReceiptError::InvalidProofMetadata( "proof.proof_reference", ))?; - validate_required_str(body, "proof.proof_reference") - .map_err(|_| RadrootsValidationReceiptError::InvalidProofMetadata("proof.proof_reference")) + if digest.len() != 64 || !is_lower_hex(digest) { + return Err(RadrootsValidationReceiptError::InvalidProofMetadata( + "proof.proof_reference", + )); + } + Ok(()) } fn validate_result_error_bitmap( @@ -741,7 +729,7 @@ mod tests { inline_proof_base64: None, mode: Some("core".to_string()), program_hash: Some(hash32('a')), - proof_reference: Some("radroots-proof://proof-1".to_string()), + proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), system: RadrootsValidationReceiptProofSystem::Sp1Core, verifying_key_hash: Some(hash32('b')), }; @@ -902,7 +890,7 @@ mod tests { )) ); - receipt.proof.proof_reference = Some("radroots-proof://proof-1".to_string()); + receipt.proof.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); let parts = validation_receipt_event_build("order-1", &receipt).expect("sp1 event parts"); let mut event = sample_validation_receipt_event(); event.content = parts.content; @@ -966,6 +954,27 @@ mod tests { invalid_inline.proof.inline_proof_base64 = Some("cHJvb2Y=".to_string()); invalid_inline.validate().expect("valid inline proof shape"); + invalid_inline.proof.inline_proof_base64 = Some("AA==".to_string()); + invalid_inline + .validate() + .expect("canonical zero byte inline proof shape"); + + invalid_inline.proof.inline_proof_base64 = Some("AB==".to_string()); + assert_eq!( + invalid_inline.validate(), + Err(RadrootsValidationReceiptError::InvalidProofMetadata( + "proof.inline_proof_base64" + )) + ); + + invalid_inline.proof.inline_proof_base64 = Some(String::new()); + assert_eq!( + invalid_inline.validate(), + Err(RadrootsValidationReceiptError::EmptyField( + "proof.inline_proof_base64" + )) + ); + let mut invalid_reference = sample_sp1_reference_receipt(); invalid_reference.proof.proof_reference = Some("https://example.test/proof".to_string()); assert_eq!( @@ -982,6 +991,39 @@ mod tests { "proof.proof_reference" )) ); + + invalid_reference.proof.proof_reference = + Some(format!("radroots-proof://sha256/{}", "A".repeat(64))); + assert_eq!( + invalid_reference.validate(), + Err(RadrootsValidationReceiptError::InvalidProofMetadata( + "proof.proof_reference" + )) + ); + + invalid_reference.proof.proof_reference = + Some(format!("radroots-proof://sha256/{}", "1".repeat(63))); + assert_eq!( + invalid_reference.validate(), + Err(RadrootsValidationReceiptError::InvalidProofMetadata( + "proof.proof_reference" + )) + ); + + invalid_reference.proof.proof_reference = + Some(format!("radroots-proof://sha256/{}/proof", "1".repeat(64))); + assert_eq!( + invalid_reference.validate(), + Err(RadrootsValidationReceiptError::InvalidProofMetadata( + "proof.proof_reference" + )) + ); + + invalid_reference.proof.proof_reference = + Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); + invalid_reference + .validate() + .expect("valid sha256 proof reference"); } #[test]