Skip to main content

syncweb_core/net/
membership_doc.rs

1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
2use iroh_docs::NamespaceId;
3use serde::{Deserialize, Serialize};
4
5use crate::{Result, SyncwebError};
6
7/// Domain separation context for membership list signatures.
8const MEMBER_LIST_SIGNATURE_CONTEXT: &[u8] = b"syncweb/network-membership/v1\0";
9
10/// The canonical, owner-signed list of network members.
11#[derive(Clone, Debug, Serialize, Deserialize)]
12#[non_exhaustive]
13pub struct SignedMemberList {
14    pub network_id: String,
15    pub owner: String,
16    pub sequence: u64,
17    pub members: Vec<MemberEntry>,
18    pub updated_at: u64,
19    pub signature: String,
20}
21
22/// A single member entry in the member list.
23#[derive(Clone, Debug, Serialize, Deserialize)]
24#[non_exhaustive]
25pub struct MemberEntry {
26    pub key: String,
27    pub joined_at: u64,
28    pub role: MemberRole,
29}
30
31/// Role of a member in the network.
32#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
33#[non_exhaustive]
34pub enum MemberRole {
35    Admin,
36    Member,
37}
38
39impl SignedMemberList {
40    /// Create a new empty member list for the given network and owner.
41    #[must_use]
42    pub fn new(network_id: &str, owner: &str) -> Self {
43        Self {
44            network_id: network_id.to_owned(),
45            owner: owner.to_owned(),
46            sequence: 0,
47            members: Vec::new(),
48            updated_at: current_unix_secs(),
49            signature: String::new(),
50        }
51    }
52
53    /// Sign this member list with the given signing key.
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if serialization fails.
58    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
59        let owner_hex = hex::encode(signing_key.verifying_key().to_bytes());
60        if self.owner != owner_hex {
61            return Err(SyncwebError::InvalidSignature(
62                "signing key does not match owner field".to_owned(),
63            ));
64        }
65        let message = self.serialize_unsigned()?;
66        let mut signed_bytes = Vec::new();
67        signed_bytes.extend_from_slice(MEMBER_LIST_SIGNATURE_CONTEXT);
68        signed_bytes.extend_from_slice(&message);
69        self.signature = hex::encode(signing_key.sign(&signed_bytes).to_bytes());
70        Ok(())
71    }
72
73    /// Verify the signature on this member list.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error if the signature is missing, malformed, or invalid.
78    pub fn verify(&self) -> Result<()> {
79        let key_bytes = hex::decode(&self.owner)
80            .map_err(|error| SyncwebError::InvalidSignature(format!("invalid owner key hex: {error}")))?;
81        let key_array: [u8; 32] = key_bytes
82            .try_into()
83            .map_err(|_length_error| SyncwebError::InvalidSignature("owner key must be 32 bytes".to_owned()))?;
84        let verifying_key = VerifyingKey::from_bytes(&key_array)
85            .map_err(|error| SyncwebError::InvalidSignature(format!("invalid owner key: {error}")))?;
86        let sig_bytes = hex::decode(&self.signature)
87            .map_err(|error| SyncwebError::InvalidSignature(format!("invalid signature hex: {error}")))?;
88        let sig_array: [u8; 64] = sig_bytes
89            .try_into()
90            .map_err(|_length_error| SyncwebError::InvalidSignature("signature must be 64 bytes".to_owned()))?;
91        let signature = Signature::from_bytes(&sig_array);
92        let message = self.serialize_unsigned()?;
93        let mut signed_bytes = Vec::new();
94        signed_bytes.extend_from_slice(MEMBER_LIST_SIGNATURE_CONTEXT);
95        signed_bytes.extend_from_slice(&message);
96        verifying_key
97            .verify(&signed_bytes, &signature)
98            .map_err(|error| SyncwebError::InvalidSignature(format!("signature verification failed: {error}")))
99    }
100
101    /// Serialize without the signature field.
102    fn serialize_unsigned(&self) -> Result<Vec<u8>> {
103        let mut unsigned = self.clone();
104        unsigned.signature = String::new();
105        serde_json::to_vec(&unsigned).map_err(|error| SyncwebError::operation("failed to serialize member list", error))
106    }
107}
108
109/// Derive the deterministic Iroh docs namespace for a network.
110///
111/// Used by the owner at network creation time. New members receive the
112/// namespace via the `doc_ticket` in the `NetworkTicket`.
113#[must_use]
114pub fn network_doc_namespace(network_id: &[u8; 32], shared_secret: &[u8; 32]) -> NamespaceId {
115    let mut hasher = blake3::Hasher::new();
116    hasher.update(b"syncweb/network-doc/v1\0");
117    hasher.update(network_id);
118    hasher.update(shared_secret);
119    NamespaceId::from(hasher.finalize().as_bytes())
120}
121
122/// Network metadata stored in the membership doc.
123#[derive(Clone, Debug, Serialize, Deserialize)]
124#[non_exhaustive]
125pub struct NetworkInfo {
126    pub name: String,
127    pub label: String,
128    pub created_at: u64,
129}
130
131fn current_unix_secs() -> u64 {
132    std::time::SystemTime::now()
133        .duration_since(std::time::UNIX_EPOCH)
134        .map_or(0, |d| d.as_secs())
135}