saorsa_core/mls/
mod.rs

1// MLS verifier scaffolding and proof format
2
3// Temporary stub - will be replaced with actual implementation
4#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
5pub struct GroupIdentityPacketV1 {
6    pub id: crate::fwid::Key,
7    pub members: Vec<GroupMember>,
8    pub group_pk: Vec<u8>,
9}
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct GroupMember {
13    pub member_id: crate::fwid::Key,
14    pub member_pk: Vec<u8>,
15}
16use anyhow::Result;
17use saorsa_pqc::MlDsaOperations; // bring trait into scope for verify()
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20use std::sync::Arc;
21
22/// Provider that supplies a snapshot of a group's identity packet
23/// given a group_id (words hash).
24pub trait MlsGroupStateProvider: Send + Sync {
25    fn fetch_group_identity(&self, group_id: &[u8]) -> Result<GroupIdentityPacketV1>;
26}
27
28/// Supported proof modes
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub enum ProofMode {
31    Member,
32    Group,
33}
34
35/// CBOR-encoded MLS proof (version 1)
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct MlsProofV1 {
38    pub ver: u8,                      // must be 1
39    pub mode: ProofMode,              // "member" or "group"
40    pub cipher: u16,                  // ciphersuite id
41    pub epoch: u64,                   // epoch being proven
42    pub signer_id: Option<Vec<u8>>,   // member id for Member mode
43    pub key_id: Option<Vec<u8>>,      // external key id for Group mode
44    pub sig: Vec<u8>,                 // signature bytes
45    pub roster_hash: Option<Vec<u8>>, // optional roster hash pin
46    pub signer_pub: Option<Vec<u8>>,  // optional signer pub override
47}
48
49/// Default verifier using a provided group-state provider.
50pub struct DefaultMlsVerifier {
51    provider: Arc<dyn MlsGroupStateProvider>,
52}
53
54impl DefaultMlsVerifier {
55    pub fn new(provider: Arc<dyn MlsGroupStateProvider>) -> Self {
56        Self { provider }
57    }
58
59    fn make_msg(group_id: &[u8], epoch: u64, record: &[u8]) -> Vec<u8> {
60        const DST: &[u8] = b"saorsa-mls:dht-proof:v1";
61        let mut hasher = Sha256::new();
62        hasher.update(record);
63        let record_hash = hasher.finalize();
64        let mut msg = Vec::with_capacity(DST.len() + group_id.len() + 8 + 32);
65        msg.extend_from_slice(DST);
66        msg.extend_from_slice(group_id);
67        msg.extend_from_slice(&epoch.to_be_bytes());
68        msg.extend_from_slice(&record_hash);
69        msg
70    }
71}
72
73impl crate::auth::MlsProofVerifier for DefaultMlsVerifier {
74    fn verify(&self, group_id: &[u8], epoch: u64, proof: &[u8], record: &[u8]) -> Result<bool> {
75        // Parse CBOR proof
76        let proof: MlsProofV1 = match serde_cbor::from_slice(proof) {
77            Ok(p) => p,
78            Err(_) => return Ok(false),
79        };
80        if proof.ver != 1 || proof.epoch != epoch {
81            return Ok(false);
82        }
83
84        // Fetch and validate the group identity snapshot
85        let group = self.provider.fetch_group_identity(group_id)?;
86        // Sanity: group id must match
87        if group.id.as_bytes() != group_id {
88            return Ok(false);
89        }
90
91        // Build canonical MLS message
92        let msg = Self::make_msg(group_id, epoch, record);
93
94        // Verify based on mode
95        match proof.mode {
96            ProofMode::Member => {
97                // Resolve signer public key: proof.signer_pub OR lookup via signer_id in roster
98                let signer_pk_bytes = if let Some(pk) = proof.signer_pub.as_ref() {
99                    pk.as_slice()
100                } else {
101                    // Find member by signer_id
102                    let sid = match &proof.signer_id {
103                        Some(s) => s,
104                        None => return Ok(false),
105                    };
106                    let member = match group
107                        .members
108                        .iter()
109                        .find(|m| m.member_id.as_bytes() == sid.as_slice())
110                    {
111                        Some(m) => m,
112                        None => return Ok(false),
113                    };
114                    member.member_pk.as_slice()
115                };
116
117                // Verify signature using saorsa-pqc ML-DSA
118                let pk = match crate::quantum_crypto::MlDsaPublicKey::from_bytes(signer_pk_bytes) {
119                    Ok(p) => p,
120                    Err(_) => return Ok(false),
121                };
122                // Expect fixed ML-DSA-65 signature length
123                const SIG_LEN: usize = 3309;
124                if proof.sig.len() != SIG_LEN {
125                    return Ok(false);
126                }
127                let mut arr = [0u8; SIG_LEN];
128                arr.copy_from_slice(&proof.sig);
129                let sig = crate::quantum_crypto::MlDsaSignature(Box::new(arr));
130                let ml = crate::quantum_crypto::MlDsa65::new();
131                let ok = ml.verify(&pk, &msg, &sig).unwrap_or(false);
132                Ok(ok)
133            }
134            ProofMode::Group => {
135                // Verify with group external key (not stored yet) – fallback to group_pk from identity
136                let pk = match crate::quantum_crypto::MlDsaPublicKey::from_bytes(&group.group_pk) {
137                    Ok(p) => p,
138                    Err(_) => return Ok(false),
139                };
140                const SIG_LEN: usize = 3309;
141                if proof.sig.len() != SIG_LEN {
142                    return Ok(false);
143                }
144                let mut arr = [0u8; SIG_LEN];
145                arr.copy_from_slice(&proof.sig);
146                let sig = crate::quantum_crypto::MlDsaSignature(Box::new(arr));
147                let ml = crate::quantum_crypto::MlDsa65::new();
148                let ok = ml.verify(&pk, &msg, &sig).unwrap_or(false);
149                Ok(ok)
150            }
151        }
152    }
153}