Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
479
vendor/ruvector/examples/edge/src/p2p/envelope.rs
vendored
Normal file
479
vendor/ruvector/examples/edge/src/p2p/envelope.rs
vendored
Normal file
@@ -0,0 +1,479 @@
|
||||
//! Message Envelopes - Signed and Encrypted Messages
|
||||
//!
|
||||
//! Security principles:
|
||||
//! - All messages are signed with Ed25519
|
||||
//! - Signatures cover canonical representation of all fields
|
||||
//! - TaskReceipt includes full execution binding
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_big_array::BigArray;
|
||||
use crate::p2p::crypto::{EncryptedPayload, CanonicalJson, CryptoV2};
|
||||
|
||||
/// Signed message envelope for P2P communication
|
||||
///
|
||||
/// Design:
|
||||
/// - Header fields are signed but not encrypted
|
||||
/// - Payload is encrypted with swarm key (for broadcast) or session key (for direct)
|
||||
/// - Sender identity verified via registry, NOT from this envelope
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SignedEnvelope {
|
||||
// Header (signed but not encrypted)
|
||||
pub message_id: String,
|
||||
pub topic: String,
|
||||
pub timestamp: u64,
|
||||
pub sender_id: String,
|
||||
#[serde(with = "hex::serde")]
|
||||
pub payload_hash: [u8; 32],
|
||||
pub nonce: String,
|
||||
pub counter: u64,
|
||||
|
||||
// Signature covers canonical representation of all header fields
|
||||
#[serde(with = "BigArray")]
|
||||
pub signature: [u8; 64],
|
||||
|
||||
// Encrypted payload (swarm key or session key)
|
||||
pub encrypted: EncryptedPayload,
|
||||
}
|
||||
|
||||
impl SignedEnvelope {
|
||||
/// Create canonical representation of header for signing
|
||||
/// Keys are sorted alphabetically for deterministic output
|
||||
pub fn canonical_header(&self) -> String {
|
||||
// Create a struct with sorted fields for canonical serialization
|
||||
let header = serde_json::json!({
|
||||
"counter": self.counter,
|
||||
"message_id": self.message_id,
|
||||
"nonce": self.nonce,
|
||||
"payload_hash": hex::encode(self.payload_hash),
|
||||
"sender_id": self.sender_id,
|
||||
"timestamp": self.timestamp,
|
||||
"topic": self.topic,
|
||||
});
|
||||
CanonicalJson::stringify(&header)
|
||||
}
|
||||
|
||||
/// Create unsigned envelope (for signing externally)
|
||||
pub fn new_unsigned(
|
||||
message_id: String,
|
||||
topic: String,
|
||||
sender_id: String,
|
||||
payload: &[u8],
|
||||
nonce: String,
|
||||
counter: u64,
|
||||
encrypted: EncryptedPayload,
|
||||
) -> Self {
|
||||
Self {
|
||||
message_id,
|
||||
topic,
|
||||
timestamp: chrono::Utc::now().timestamp_millis() as u64,
|
||||
sender_id,
|
||||
payload_hash: CryptoV2::hash(payload),
|
||||
nonce,
|
||||
counter,
|
||||
signature: [0u8; 64],
|
||||
encrypted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Task execution envelope with resource budgets
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskEnvelope {
|
||||
pub task_id: String,
|
||||
pub module_cid: String, // WASM module location
|
||||
pub entrypoint: String, // Function to call
|
||||
pub input_cid: String, // Input data location
|
||||
#[serde(with = "hex::serde")]
|
||||
pub output_schema_hash: [u8; 32],
|
||||
|
||||
/// Resource budgets for sandbox
|
||||
pub budgets: TaskBudgets,
|
||||
|
||||
/// Requester info
|
||||
pub requester: String,
|
||||
pub deadline: u64,
|
||||
pub priority: u8,
|
||||
|
||||
/// Canonical hash of this envelope (for receipts)
|
||||
#[serde(with = "hex::serde")]
|
||||
pub envelope_hash: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskBudgets {
|
||||
pub fuel_limit: u64, // Wasmtime fuel
|
||||
pub memory_mb: u32, // Max memory in MB
|
||||
pub timeout_ms: u64, // Max execution time
|
||||
}
|
||||
|
||||
impl TaskEnvelope {
|
||||
/// Create task envelope with computed hash
|
||||
pub fn new(
|
||||
task_id: String,
|
||||
module_cid: String,
|
||||
entrypoint: String,
|
||||
input_cid: String,
|
||||
output_schema_hash: [u8; 32],
|
||||
budgets: TaskBudgets,
|
||||
requester: String,
|
||||
deadline: u64,
|
||||
priority: u8,
|
||||
) -> Self {
|
||||
let mut envelope = Self {
|
||||
task_id,
|
||||
module_cid,
|
||||
entrypoint,
|
||||
input_cid,
|
||||
output_schema_hash,
|
||||
budgets,
|
||||
requester,
|
||||
deadline,
|
||||
priority,
|
||||
envelope_hash: [0u8; 32],
|
||||
};
|
||||
|
||||
// Compute envelope hash (excluding envelope_hash field)
|
||||
envelope.envelope_hash = envelope.compute_hash();
|
||||
envelope
|
||||
}
|
||||
|
||||
/// Compute canonical hash of envelope (excluding envelope_hash itself)
|
||||
fn compute_hash(&self) -> [u8; 32] {
|
||||
let for_hash = serde_json::json!({
|
||||
"budgets": {
|
||||
"fuel_limit": self.budgets.fuel_limit,
|
||||
"memory_mb": self.budgets.memory_mb,
|
||||
"timeout_ms": self.budgets.timeout_ms,
|
||||
},
|
||||
"deadline": self.deadline,
|
||||
"entrypoint": self.entrypoint,
|
||||
"input_cid": self.input_cid,
|
||||
"module_cid": self.module_cid,
|
||||
"output_schema_hash": hex::encode(self.output_schema_hash),
|
||||
"priority": self.priority,
|
||||
"requester": self.requester,
|
||||
"task_id": self.task_id,
|
||||
});
|
||||
let canonical = CanonicalJson::stringify(&for_hash);
|
||||
CryptoV2::hash(canonical.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// Task result receipt with full execution binding
|
||||
///
|
||||
/// Security: Signature covers ALL fields to prevent tampering
|
||||
/// Including binding to original TaskEnvelope for traceability
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskReceipt {
|
||||
// Core result
|
||||
pub task_id: String,
|
||||
pub executor: String,
|
||||
pub result_cid: String,
|
||||
pub status: TaskStatus,
|
||||
|
||||
// Resource usage
|
||||
pub fuel_used: u64,
|
||||
pub memory_peak_mb: u32,
|
||||
pub execution_ms: u64,
|
||||
|
||||
// Execution binding (proves this receipt is for this specific execution)
|
||||
#[serde(with = "hex::serde")]
|
||||
pub input_hash: [u8; 32],
|
||||
#[serde(with = "hex::serde")]
|
||||
pub output_hash: [u8; 32],
|
||||
#[serde(with = "hex::serde")]
|
||||
pub module_hash: [u8; 32],
|
||||
pub start_timestamp: u64,
|
||||
pub end_timestamp: u64,
|
||||
|
||||
// TaskEnvelope binding (proves this receipt matches original task)
|
||||
pub module_cid: String,
|
||||
pub input_cid: String,
|
||||
pub entrypoint: String,
|
||||
#[serde(with = "hex::serde")]
|
||||
pub output_schema_hash: [u8; 32],
|
||||
#[serde(with = "hex::serde")]
|
||||
pub task_envelope_hash: [u8; 32],
|
||||
|
||||
// Signature covers ALL fields above
|
||||
#[serde(with = "BigArray")]
|
||||
pub signature: [u8; 64],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum TaskStatus {
|
||||
Success,
|
||||
Error,
|
||||
Timeout,
|
||||
OutOfMemory,
|
||||
}
|
||||
|
||||
impl TaskReceipt {
|
||||
/// Create canonical representation for signing
|
||||
/// Includes ALL fields for full execution binding
|
||||
pub fn canonical_for_signing(&self) -> String {
|
||||
let for_signing = serde_json::json!({
|
||||
"end_timestamp": self.end_timestamp,
|
||||
"entrypoint": self.entrypoint,
|
||||
"execution_ms": self.execution_ms,
|
||||
"executor": self.executor,
|
||||
"fuel_used": self.fuel_used,
|
||||
"input_cid": self.input_cid,
|
||||
"input_hash": hex::encode(self.input_hash),
|
||||
"memory_peak_mb": self.memory_peak_mb,
|
||||
"module_cid": self.module_cid,
|
||||
"module_hash": hex::encode(self.module_hash),
|
||||
"output_hash": hex::encode(self.output_hash),
|
||||
"output_schema_hash": hex::encode(self.output_schema_hash),
|
||||
"result_cid": self.result_cid,
|
||||
"start_timestamp": self.start_timestamp,
|
||||
"status": format!("{:?}", self.status),
|
||||
"task_envelope_hash": hex::encode(self.task_envelope_hash),
|
||||
"task_id": self.task_id,
|
||||
});
|
||||
CanonicalJson::stringify(&for_signing)
|
||||
}
|
||||
|
||||
/// Create unsigned receipt (for signing externally)
|
||||
pub fn new_unsigned(
|
||||
task: &TaskEnvelope,
|
||||
executor: String,
|
||||
result_cid: String,
|
||||
status: TaskStatus,
|
||||
fuel_used: u64,
|
||||
memory_peak_mb: u32,
|
||||
execution_ms: u64,
|
||||
input_hash: [u8; 32],
|
||||
output_hash: [u8; 32],
|
||||
module_hash: [u8; 32],
|
||||
start_timestamp: u64,
|
||||
end_timestamp: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_id: task.task_id.clone(),
|
||||
executor,
|
||||
result_cid,
|
||||
status,
|
||||
fuel_used,
|
||||
memory_peak_mb,
|
||||
execution_ms,
|
||||
input_hash,
|
||||
output_hash,
|
||||
module_hash,
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
module_cid: task.module_cid.clone(),
|
||||
input_cid: task.input_cid.clone(),
|
||||
entrypoint: task.entrypoint.clone(),
|
||||
output_schema_hash: task.output_schema_hash,
|
||||
task_envelope_hash: task.envelope_hash,
|
||||
signature: [0u8; 64],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Signaling message for WebRTC (via GUN)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SignalingMessage {
|
||||
pub signal_type: SignalType,
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
pub payload: String,
|
||||
#[serde(with = "hex::serde")]
|
||||
pub payload_hash: [u8; 32],
|
||||
pub timestamp: u64,
|
||||
pub expires_at: u64,
|
||||
#[serde(with = "BigArray")]
|
||||
pub signature: [u8; 64],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum SignalType {
|
||||
Offer,
|
||||
Answer,
|
||||
Ice,
|
||||
}
|
||||
|
||||
impl SignalingMessage {
|
||||
/// Create canonical representation for signing
|
||||
/// Does NOT include sender_pubkey - verified via registry
|
||||
pub fn canonical_for_signing(&self) -> String {
|
||||
let for_signing = serde_json::json!({
|
||||
"expires_at": self.expires_at,
|
||||
"from": self.from,
|
||||
"payload_hash": hex::encode(self.payload_hash),
|
||||
"signal_type": format!("{:?}", self.signal_type),
|
||||
"timestamp": self.timestamp,
|
||||
"to": self.to,
|
||||
});
|
||||
CanonicalJson::stringify(&for_signing)
|
||||
}
|
||||
|
||||
/// Create unsigned signaling message
|
||||
pub fn new_unsigned(
|
||||
signal_type: SignalType,
|
||||
from: String,
|
||||
to: String,
|
||||
payload: String,
|
||||
ttl_ms: u64,
|
||||
) -> Self {
|
||||
let now = chrono::Utc::now().timestamp_millis() as u64;
|
||||
Self {
|
||||
signal_type,
|
||||
from,
|
||||
to,
|
||||
payload_hash: CryptoV2::hash(payload.as_bytes()),
|
||||
payload,
|
||||
timestamp: now,
|
||||
expires_at: now + ttl_ms,
|
||||
signature: [0u8; 64],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if signal is expired
|
||||
pub fn is_expired(&self) -> bool {
|
||||
let now = chrono::Utc::now().timestamp_millis() as u64;
|
||||
now > self.expires_at
|
||||
}
|
||||
}
|
||||
|
||||
/// Artifact pointer (small metadata that goes to GUN)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArtifactPointer {
|
||||
pub artifact_type: ArtifactType,
|
||||
pub agent_id: String,
|
||||
pub cid: String,
|
||||
pub version: u32,
|
||||
#[serde(with = "hex::serde")]
|
||||
pub schema_hash: [u8; 8],
|
||||
pub dimensions: String,
|
||||
#[serde(with = "hex::serde")]
|
||||
pub checksum: [u8; 16],
|
||||
pub timestamp: u64,
|
||||
#[serde(with = "BigArray")]
|
||||
pub signature: [u8; 64],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ArtifactType {
|
||||
QTable,
|
||||
MemoryVectors,
|
||||
ModelWeights,
|
||||
Trajectory,
|
||||
}
|
||||
|
||||
impl ArtifactPointer {
|
||||
/// Create canonical representation for signing
|
||||
pub fn canonical_for_signing(&self) -> String {
|
||||
let for_signing = serde_json::json!({
|
||||
"agent_id": self.agent_id,
|
||||
"artifact_type": format!("{:?}", self.artifact_type),
|
||||
"checksum": hex::encode(self.checksum),
|
||||
"cid": self.cid,
|
||||
"dimensions": self.dimensions,
|
||||
"schema_hash": hex::encode(self.schema_hash),
|
||||
"timestamp": self.timestamp,
|
||||
"version": self.version,
|
||||
});
|
||||
CanonicalJson::stringify(&for_signing)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_envelope_canonical_header() {
|
||||
let encrypted = EncryptedPayload {
|
||||
ciphertext: vec![1, 2, 3],
|
||||
iv: [0u8; 12],
|
||||
tag: [0u8; 16],
|
||||
};
|
||||
|
||||
let envelope = SignedEnvelope::new_unsigned(
|
||||
"msg-001".to_string(),
|
||||
"test-topic".to_string(),
|
||||
"sender-001".to_string(),
|
||||
b"test payload",
|
||||
"abc123".to_string(),
|
||||
1,
|
||||
encrypted,
|
||||
);
|
||||
|
||||
let canonical1 = envelope.canonical_header();
|
||||
let canonical2 = envelope.canonical_header();
|
||||
|
||||
// Must be deterministic
|
||||
assert_eq!(canonical1, canonical2);
|
||||
|
||||
// Must contain all fields
|
||||
assert!(canonical1.contains("msg-001"));
|
||||
assert!(canonical1.contains("test-topic"));
|
||||
assert!(canonical1.contains("sender-001"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_receipt_includes_all_binding_fields() {
|
||||
let task = TaskEnvelope::new(
|
||||
"task-001".to_string(),
|
||||
"local:abc123".to_string(),
|
||||
"process".to_string(),
|
||||
"local:input123".to_string(),
|
||||
[0u8; 32],
|
||||
TaskBudgets {
|
||||
fuel_limit: 1000000,
|
||||
memory_mb: 128,
|
||||
timeout_ms: 30000,
|
||||
},
|
||||
"requester-001".to_string(),
|
||||
chrono::Utc::now().timestamp_millis() as u64 + 60000,
|
||||
1,
|
||||
);
|
||||
|
||||
let receipt = TaskReceipt::new_unsigned(
|
||||
&task,
|
||||
"executor-001".to_string(),
|
||||
"local:result123".to_string(),
|
||||
TaskStatus::Success,
|
||||
500000,
|
||||
64,
|
||||
1500,
|
||||
[1u8; 32],
|
||||
[2u8; 32],
|
||||
[3u8; 32],
|
||||
1000,
|
||||
2500,
|
||||
);
|
||||
|
||||
let canonical = receipt.canonical_for_signing();
|
||||
|
||||
// Must include execution binding fields
|
||||
assert!(canonical.contains("input_hash"));
|
||||
assert!(canonical.contains("output_hash"));
|
||||
assert!(canonical.contains("module_hash"));
|
||||
|
||||
// Must include task envelope binding
|
||||
assert!(canonical.contains("module_cid"));
|
||||
assert!(canonical.contains("input_cid"));
|
||||
assert!(canonical.contains("entrypoint"));
|
||||
assert!(canonical.contains("task_envelope_hash"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signaling_message_no_pubkey_in_signature() {
|
||||
let signal = SignalingMessage::new_unsigned(
|
||||
SignalType::Offer,
|
||||
"alice".to_string(),
|
||||
"bob".to_string(),
|
||||
"sdp data here".to_string(),
|
||||
60000,
|
||||
);
|
||||
|
||||
let canonical = signal.canonical_for_signing();
|
||||
|
||||
// Should NOT contain any pubkey field
|
||||
assert!(!canonical.contains("pubkey"));
|
||||
assert!(!canonical.contains("public_key"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user