commucat_federation/
lib.rs

1use blake3::Hasher;
2use chrono::{DateTime, Utc};
3use commucat_crypto::{EventSigner, EventVerifier};
4use serde::{Deserialize, Serialize};
5use std::error::Error;
6use std::fmt::{Display, Formatter};
7
8#[derive(Debug)]
9pub enum FederationError {
10    Signature,
11    DigestMismatch,
12}
13
14impl Display for FederationError {
15    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
16        match self {
17            Self::Signature => write!(f, "signature validation failed"),
18            Self::DigestMismatch => write!(f, "digest mismatch"),
19        }
20    }
21}
22
23impl Error for FederationError {}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct FederationEvent {
27    pub event_id: String,
28    pub origin: String,
29    pub created_at: DateTime<Utc>,
30    pub payload: serde_json::Value,
31    pub scope: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct SignedEvent {
36    pub event: FederationEvent,
37    pub signature: Vec<u8>,
38    pub digest: Vec<u8>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct PeerDescriptor {
43    pub domain: String,
44    pub endpoint: String,
45    pub public_key: [u8; 32],
46    pub last_seen: Option<DateTime<Utc>>,
47}
48
49/// Computes a BLAKE3 digest for a federation event.
50pub fn digest_event(event: &FederationEvent) -> [u8; 32] {
51    let mut hasher = Hasher::new();
52    hasher.update(event.event_id.as_bytes());
53    hasher.update(event.origin.as_bytes());
54    hasher.update(event.scope.as_bytes());
55    let timestamp = event.created_at.timestamp_nanos_opt().unwrap_or_default();
56    hasher.update(&timestamp.to_le_bytes());
57    hasher.update(event.payload.to_string().as_bytes());
58    let hash = hasher.finalize();
59    let mut digest = [0u8; 32];
60    digest.copy_from_slice(hash.as_bytes());
61    digest
62}
63
64/// Produces a signed event envelope suitable for federation transport.
65pub fn sign_event(event: FederationEvent, signer: &EventSigner) -> SignedEvent {
66    let digest = digest_event(&event);
67    let signature = signer.sign(&digest);
68    SignedEvent {
69        event,
70        signature: signature.to_vec(),
71        digest: digest.to_vec(),
72    }
73}
74
75/// Validates a signed event against a known peer descriptor.
76pub fn verify_event(signed: &SignedEvent, verifier: &EventVerifier) -> Result<(), FederationError> {
77    let digest = digest_event(&signed.event);
78    if signed.digest.as_slice() != digest {
79        return Err(FederationError::DigestMismatch);
80    }
81    if signed.signature.len() != 64 {
82        return Err(FederationError::Signature);
83    }
84    let mut sig = [0u8; 64];
85    sig.copy_from_slice(&signed.signature);
86    verifier
87        .verify(&digest, &sig)
88        .map_err(|_| FederationError::Signature)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use commucat_crypto::DeviceKeyPair;
95
96    #[test]
97    fn sign_verify_roundtrip() {
98        let keys = DeviceKeyPair::from_seed(b"federation-federation-federation-seed").unwrap();
99        let signer = EventSigner::new(&keys);
100        let verifier = EventVerifier {
101            public: keys.public,
102        };
103        let event = FederationEvent {
104            event_id: "evt-1".to_string(),
105            origin: "example.org".to_string(),
106            created_at: Utc::now(),
107            payload: serde_json::json!({"channel": "123", "payload": "cipher"}),
108            scope: "relay".to_string(),
109        };
110        let signed = sign_event(event, &signer);
111        verify_event(&signed, &verifier).unwrap();
112    }
113
114    #[test]
115    fn verify_event_rejects_tampered_payload() {
116        let keys = DeviceKeyPair::from_seed(b"tamper-detection-seed-tamper-detect").unwrap();
117        let signer = EventSigner::new(&keys);
118        let verifier = EventVerifier {
119            public: keys.public,
120        };
121        let event = FederationEvent {
122            event_id: "evt-2".to_string(),
123            origin: "example.org".to_string(),
124            created_at: Utc::now(),
125            payload: serde_json::json!({"channel": "1337", "payload": "cipher"}),
126            scope: "relay".to_string(),
127        };
128        let mut signed = sign_event(event, &signer);
129        signed.event.payload = serde_json::json!({"channel": "1337", "payload": "altered"});
130        assert!(verify_event(&signed, &verifier).is_err());
131    }
132}