gun/sea/
certify.rs

1//! Certificate-Based Access Control
2//! Based on Gun.js sea/certify.js
3//! Allows an authority to grant permissions to certificants for specific paths/patterns
4
5use super::{sign, KeyPair, SeaError};
6use serde_json::{json, Value};
7use std::collections::HashMap;
8
9/// Certificate structure matching Gun.js format
10/// Contains certificants, policies (read/write), expiry, and blocks
11#[derive(Clone, Debug)]
12pub struct Certificate {
13    /// Certificants who have the rights (can be "*" for wildcard or list of pub keys)
14    pub certificants: Certificants,
15    /// Expiry timestamp (optional)
16    pub expiry: Option<f64>,
17    /// Read policy (optional)
18    pub read_policy: Option<Policy>,
19    /// Write policy (optional)
20    pub write_policy: Option<Policy>,
21    /// Read block/blacklist (optional)
22    pub read_block: Option<String>,
23    /// Write block/blacklist (optional)
24    pub write_block: Option<String>,
25}
26
27/// Certificants can be wildcard or a list of public keys
28#[derive(Clone, Debug)]
29pub enum Certificants {
30    /// Wildcard: anyone can have rights
31    Wildcard,
32    /// List of public keys
33    List(Vec<String>),
34}
35
36/// Policy for access control
37/// Can be a string, RAD/LEX object, or array of policies
38#[derive(Clone, Debug)]
39pub enum Policy {
40    /// Simple string policy (e.g., "inbox")
41    String(String),
42    /// RAD/LEX object with pattern matching
43    Radix(RadixPolicy),
44    /// Array of policies
45    Array(Vec<Policy>),
46}
47
48/// RAD/LEX pattern matching policy
49/// Supports patterns like '*', '>', '<', '?', etc.
50#[derive(Clone, Debug)]
51pub struct RadixPolicy {
52    /// Pattern map (key -> pattern)
53    pub patterns: HashMap<String, String>,
54}
55
56/// Options for certificate creation
57#[derive(Clone, Debug, Default)]
58pub struct CertifyOptions {
59    /// Expiry timestamp (optional)
60    pub expiry: Option<f64>,
61    /// Block/blacklist (optional)
62    pub block: Option<BlockPolicy>,
63    /// If true, return raw certificate without "SEA" prefix
64    pub raw: bool,
65}
66
67/// Block/blacklist policy
68#[derive(Clone, Debug)]
69pub struct BlockPolicy {
70    /// Read block
71    pub read: Option<String>,
72    /// Write block
73    pub write: Option<String>,
74}
75
76
77/// Create a certificate
78/// Based on Gun.js SEA.certify()
79///
80/// # Arguments
81/// * `certificants` - Who gets the rights: "*", a pub key string, or Vec of pub keys
82/// * `policy` - Access policy: string, RAD/LEX object, or array
83/// * `authority` - Key pair of the certificate authority
84/// * `opt` - Certificate options
85///
86/// # Returns
87/// Signed certificate string (with "SEA" prefix unless raw=true)
88///
89/// # Examples
90/// ```rust
91/// use gun::sea::{certify, pair, CertifyOptions, Policy};
92///
93/// let authority = pair().await?;
94/// let cert = certify(
95///     "*", // anyone
96///     Policy::String("inbox".to_string()),
97///     &authority,
98///     CertifyOptions::default()
99/// ).await?;
100/// ```
101pub async fn certify(
102    certificants: Certificants,
103    policy: Policy,
104    authority: &KeyPair,
105    opt: CertifyOptions,
106) -> Result<String, SeaError> {
107    // Normalize certificants
108    let certs = match certificants {
109        Certificants::Wildcard => "*".to_string(),
110        Certificants::List(list) => {
111            if list.len() == 1 {
112                list[0].clone()
113            } else {
114                serde_json::to_string(&list)
115                    .map_err(|e| SeaError::Crypto(format!("Serialization error: {}", e)))?
116            }
117        }
118    };
119
120    // Extract read/write policies
121    // In Gun.js, if policy is an object with 'read' key, use it; otherwise assume write
122    let (read_policy, write_policy): (Option<String>, Option<String>) = match policy {
123        Policy::String(s) => (None, Some(s)),
124        Policy::Radix(r) => {
125            // For RAD/LEX, serialize patterns as JSON
126            (
127                None,
128                Some(serde_json::to_string(&r.patterns).unwrap_or_default()),
129            )
130        }
131        Policy::Array(policies) => {
132            // Arrays are complex, convert to JSON array
133            let policy_strs: Vec<String> = policies.iter().map(|p| format!("{:?}", p)).collect();
134            (
135                None,
136                Some(serde_json::to_string(&policy_strs).unwrap_or_default()),
137            )
138        }
139    };
140
141    // Build certificate data (matching Gun.js format)
142    let mut cert_data = json!({
143        "c": certs, // certificants
144    });
145
146    if let Some(expiry) = opt.expiry {
147        cert_data["e"] = json!(expiry);
148    }
149
150    if let Some(ref read) = read_policy {
151        cert_data["r"] = json!(read);
152    }
153
154    if let Some(ref write) = write_policy {
155        cert_data["w"] = json!(write);
156    }
157
158    if let Some(ref block) = opt.block {
159        if let Some(ref read_block) = block.read {
160            cert_data["rb"] = json!(read_block);
161        }
162        if let Some(ref write_block) = block.write {
163            cert_data["wb"] = json!(write_block);
164        }
165    }
166
167    // Sign the certificate (sign expects Value, not string)
168    let signature = sign(&cert_data, authority).await?;
169
170    // Format: "SEA{...}" unless raw (matching Gun.js format)
171    if opt.raw {
172        // Return raw signature object
173        serde_json::to_string(&signature)
174            .map_err(|e| SeaError::Crypto(format!("Serialization error: {}", e)))
175    } else {
176        // Wrap in "SEA" prefix
177        let sig_str = serde_json::to_string(&signature)
178            .map_err(|e| SeaError::Crypto(format!("Serialization error: {}", e)))?;
179        Ok(format!("SEA{}", sig_str))
180    }
181}
182
183/// Verify a certificate with full signature verification
184/// 
185/// Verifies a certificate's signature using the authority's public key and extracts
186/// the certificate data. This performs full cryptographic verification, not just JSON parsing.
187/// 
188/// # Arguments
189/// * `cert` - Certificate string (with optional "SEA" prefix)
190/// * `authority_pub` - Public key of the certificate authority in base64 x.y format
191/// 
192/// # Returns
193/// `Certificate` struct with verified data if signature is valid
194/// 
195/// # Errors
196/// - `SeaError::Crypto`: If certificate format is invalid
197/// - `SeaError::VerificationFailed`: If signature verification fails (wrong authority or tampered data)
198/// 
199/// # Security
200/// 
201/// This function performs full cryptographic signature verification using ECDSA P-256.
202/// Certificates with invalid signatures or wrong authority keys will be rejected.
203/// 
204/// # Example
205/// ```rust,no_run
206/// use gun::sea::{certify, verify_certificate, pair, Certificants, Policy, CertifyOptions};
207/// 
208/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
209/// let authority = pair().await?;
210/// 
211/// // Create certificate
212/// let cert = certify(
213///     Certificants::Wildcard,
214///     Policy::String("inbox".to_string()),
215///     &authority,
216///     CertifyOptions::default(),
217/// ).await?;
218/// 
219/// // Verify certificate
220/// let verified = verify_certificate(&cert, &authority.pub_key).await?;
221/// println!("Certificate verified for: {:?}", verified.certificants);
222/// # Ok(())
223/// # }
224/// ```
225pub async fn verify_certificate(cert: &str, authority_pub: &str) -> Result<Certificate, SeaError> {
226    // Remove "SEA" prefix if present
227    let cert_data = if let Some(stripped) = cert.strip_prefix("SEA{") {
228        // Remove "SEA{" prefix and find matching "}"
229        // The certificate is a JSON object, so we need to parse it properly
230        let mut json_str = String::from("{");
231        json_str.push_str(stripped);
232        json_str
233    } else if let Some(stripped) = cert.strip_prefix("SEA") {
234        stripped.to_string()
235    } else {
236        cert.to_string()
237    };
238
239    // Parse certificate as signed data (format: {m: message, s: signature})
240    let signed_data: Value = serde_json::from_str(&cert_data)
241        .map_err(|e| SeaError::Crypto(format!("Parse error: {}", e)))?;
242
243    // Verify signature using SEA.verify()
244    use super::verify;
245    let parsed = verify(&signed_data, authority_pub).await?;
246
247    // Extract certificate fields
248    let certificants = if let Some(c) = parsed.get("c") {
249        if let Some(s) = c.as_str() {
250            if s == "*" {
251                Certificants::Wildcard
252            } else {
253                Certificants::List(vec![s.to_string()])
254            }
255        } else if let Some(arr) = c.as_array() {
256            Certificants::List(
257                arr.iter()
258                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
259                    .collect(),
260            )
261        } else {
262            return Err(SeaError::Crypto("Invalid certificants format".to_string()));
263        }
264    } else {
265        return Err(SeaError::Crypto("Missing certificants".to_string()));
266    };
267
268    let expiry = parsed.get("e").and_then(|v| v.as_f64());
269    let read_policy = parsed
270        .get("r")
271        .and_then(|v| v.as_str().map(|s| Policy::String(s.to_string())));
272    let write_policy = parsed
273        .get("w")
274        .and_then(|v| v.as_str().map(|s| Policy::String(s.to_string())));
275    let read_block = parsed
276        .get("rb")
277        .and_then(|v| v.as_str().map(|s| s.to_string()));
278    let write_block = parsed
279        .get("wb")
280        .and_then(|v| v.as_str().map(|s| s.to_string()));
281
282    Ok(Certificate {
283        certificants,
284        expiry,
285        read_policy,
286        write_policy,
287        read_block,
288        write_block,
289    })
290}
291
292/// Check if a path matches a policy
293/// Implements RAD/LEX pattern matching
294pub fn matches_policy(path: &str, policy: &Policy) -> bool {
295    match policy {
296        Policy::String(s) => {
297            // Simple string match
298            path == s || path.starts_with(&format!("{}/", s))
299        }
300        Policy::Radix(r) => {
301            // RAD/LEX pattern matching
302            // For now, simple implementation - full RAD/LEX would need more complex matching
303            for pattern in r.patterns.keys() {
304                if pattern == "*" {
305                    return true; // Wildcard matches everything
306                }
307                if path.starts_with(pattern) {
308                    return true;
309                }
310            }
311            false
312        }
313        Policy::Array(policies) => {
314            // Array: any policy matches
315            policies.iter().any(|p| matches_policy(path, p))
316        }
317    }
318}
319
320/// Check if a certificate grants permission for a path
321pub fn check_permission(
322    cert: &Certificate,
323    path: &str,
324    operation: &str, // "read" or "write"
325) -> bool {
326    // Check expiry
327    if let Some(expiry) = cert.expiry {
328        let now = chrono::Utc::now().timestamp_millis() as f64;
329        if now > expiry {
330            return false; // Expired
331        }
332    }
333
334    // Check blocks
335    if operation == "read" {
336        if let Some(ref block) = cert.read_block {
337            if path.contains(block) {
338                return false; // Blocked
339            }
340        }
341    } else if operation == "write" {
342        if let Some(ref block) = cert.write_block {
343            if path.contains(block) {
344                return false; // Blocked
345            }
346        }
347    }
348
349    // Check policies
350    if operation == "read" {
351        if let Some(ref policy) = cert.read_policy {
352            return matches_policy(path, policy);
353        }
354    } else if operation == "write" {
355        if let Some(ref policy) = cert.write_policy {
356            return matches_policy(path, policy);
357        }
358    }
359
360    false
361}