use std::time::Duration;
use base64::Engine;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::error::OenError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageType {
Publish,
Fetch,
Feedback,
}
impl std::fmt::Display for MessageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageType::Publish => write!(f, "Publish"),
MessageType::Fetch => write!(f, "Fetch"),
MessageType::Feedback => write!(f, "Feedback"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OenEnvelope {
pub sender_id: String,
pub message_type: MessageType,
pub payload: serde_json::Value,
pub signature: String,
pub timestamp: String,
}
impl OenEnvelope {
pub fn from_json(json: &str) -> Result<Self, OenError> {
serde_json::from_str(json).map_err(|e| OenError::ParseError(e.to_string()))
}
pub fn to_json(&self) -> Result<String, OenError> {
serde_json::to_string(self).map_err(|e| OenError::ParseError(e.to_string()))
}
}
#[derive(Clone)]
pub struct OenVerifier {
timestamp_tolerance_secs: i64,
signature_cache: std::sync::Arc<tokio::sync::Mutex<std::collections::HashMap<String, Instant>>>,
}
struct Instant {
at: std::time::Instant,
}
impl Default for Instant {
fn default() -> Self {
Self {
at: std::time::Instant::now(),
}
}
}
impl OenVerifier {
pub fn new() -> Self {
Self::default()
}
pub fn with_timestamp_tolerance(mut self, secs: i64) -> Self {
self.timestamp_tolerance_secs = secs;
self
}
pub async fn verify_envelope(
&self,
envelope: &OenEnvelope,
expected_agent_id: &str,
public_key_hex: &str,
) -> Result<(), OenError> {
if envelope.message_type != MessageType::Publish {
return Err(OenError::InvalidMessageType {
expected: MessageType::Publish.to_string(),
actual: envelope.message_type.to_string(),
});
}
if envelope.sender_id != expected_agent_id {
return Err(OenError::SenderMismatch {
expected: expected_agent_id.to_string(),
actual: envelope.sender_id.clone(),
});
}
let timestamp: DateTime<Utc> = envelope
.timestamp
.parse()
.map_err(|_| OenError::ParseError("invalid timestamp format".to_string()))?;
let now = Utc::now();
let diff = (now - timestamp).num_seconds().abs();
if diff > self.timestamp_tolerance_secs {
return Err(OenError::TimestampExpired {
seconds: diff,
max: self.timestamp_tolerance_secs,
});
}
let cache_key = format!("{}:{}", envelope.sender_id, envelope.signature);
{
let cache = self.signature_cache.lock().await;
if let Some(instant) = cache.get(&cache_key) {
if instant.at.elapsed() < Duration::from_secs(300) {
return Ok(());
}
}
}
let payload_bytes = serde_json::to_vec(&envelope.payload)
.map_err(|e| OenError::ParseError(e.to_string()))?;
let signature_bytes =
base64_decode(&envelope.signature).map_err(|_| OenError::SignatureFailed)?;
use ed25519_dalek::{Signature, Verifier};
let signature_bytes: [u8; 64] = signature_bytes
.try_into()
.map_err(|_| OenError::SignatureFailed)?;
let signature = Signature::from_bytes(&signature_bytes);
let public_key_bytes = hex::decode(public_key_hex)
.map_err(|_| OenError::SigningError("invalid public key hex".to_string()))?;
let public_key_bytes: [u8; 32] = public_key_bytes
.try_into()
.map_err(|_| OenError::SigningError("expected 32-byte public key".to_string()))?;
let public_key = ed25519_dalek::VerifyingKey::from_bytes(&public_key_bytes)
.map_err(|_| OenError::SigningError("invalid public key".to_string()))?;
public_key
.verify(&payload_bytes, &signature)
.map_err(|_| OenError::SignatureFailed)?;
{
let mut cache = self.signature_cache.lock().await;
cache.insert(cache_key, Instant::default());
}
Ok(())
}
}
impl Default for OenVerifier {
fn default() -> Self {
Self {
timestamp_tolerance_secs: 300, signature_cache: std::sync::Arc::new(tokio::sync::Mutex::new(
std::collections::HashMap::new(),
)),
}
}
}
fn base64_decode(input: &str) -> Result<Vec<u8>, ()> {
base64::engine::general_purpose::STANDARD
.decode(input)
.map_err(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_type_serialization() {
let json = r#""publish""#;
let msg_type: MessageType = serde_json::from_str(json).unwrap();
assert_eq!(msg_type, MessageType::Publish);
let back_to_json = serde_json::to_string(&msg_type).unwrap();
assert_eq!(back_to_json, "\"publish\"");
}
#[tokio::test]
async fn test_envelope_parsing() {
let json = r#"{
"sender_id": "agent-123",
"message_type": "publish",
"payload": {"gene": {}},
"signature": "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01234567",
"timestamp": "2026-04-14T10:00:00Z"
}"#;
let envelope = OenEnvelope::from_json(json).unwrap();
assert_eq!(envelope.sender_id, "agent-123");
assert_eq!(envelope.message_type, MessageType::Publish);
}
}