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}