Skip to main content

beam/sea/
certify.rs

1//! SEA.certify — capability certificates for delegated authorization
2//!
3//! Based on Gun.js SEA.certify and SEA.verify.certify semantics.
4//! An authority signs a JSON payload authorizing certificants to
5//! perform actions under certain policies.
6//!
7//! Certificate format (pre-signing):
8//! ```json
9//! { "c": ["pubkey1", "pubkey2"],   // certificants
10//!   "e": 1716460800000,            // expiry (optional, ms since epoch)
11//!   "r": ".*",                     // read policy (optional)
12//!   "w": ".*",                     // write policy (optional)
13//!   "rb": "",                      // read block (optional)
14//!   "wb": "" }                     // write block (optional)
15//! ```
16//!
17//! Signed format: {m: payload_json, s: signature_b64}
18
19use super::{KeyPair, SeaError};
20use serde_json::Value as JsonValue;
21
22/// Build and sign a capability certificate.
23///
24/// # Arguments
25/// * `authority` — The keypair of the authority granting rights
26/// * `certificants` — List of pubkeys being authorized (the "c" field)
27/// * `policies` — Optional JsonValue with recognized keys: e, r, w, rb, wb
28///
29/// Returns a signed certificate in Gun.js format: `{m: ..., s: ...}`
30pub async fn certify(
31    authority: &KeyPair,
32    certificants: &[String],
33    policies: Option<&JsonValue>,
34) -> Result<JsonValue, SeaError> {
35    let mut cert = serde_json::json!({
36        "c": certificants,
37    });
38
39    if let Some(pol) = policies {
40        if let Some(obj) = pol.as_object() {
41            for key in ["e", "r", "w", "rb", "wb"] {
42                if let Some(v) = obj.get(key) {
43                    cert[key] = v.clone();
44                }
45            }
46        }
47    }
48
49    super::sign::sign(&cert, authority).await
50}
51
52/// Verify a signed certificate against an authority's public key.
53///
54/// Checks signature validity and expiry (if present).
55/// Returns the verified certificate payload.
56pub fn verify_certificate(
57    signed_cert: &JsonValue,
58    authority_pubkey: &str,
59) -> Result<JsonValue, SeaError> {
60    let payload = super::verify::verify_sync(signed_cert, authority_pubkey)?;
61
62    if let Some(expiry) = payload.get("e").and_then(|e| e.as_f64()) {
63        let now = web_time::SystemTime::now()
64            .duration_since(web_time::UNIX_EPOCH)
65            .unwrap_or_default()
66            .as_millis() as f64;
67        if expiry < now {
68            return Err(SeaError::VerificationFailed);
69        }
70    }
71
72    Ok(payload)
73}
74
75/// Check if a given pubkey appears in the certificants list.
76pub fn is_certified(payload: &JsonValue, pubkey: &str) -> bool {
77    payload
78        .get("c")
79        .and_then(|c| c.as_array())
80        .map(|arr| arr.iter().any(|v| v.as_str() == Some(pubkey)))
81        .unwrap_or(false)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::sea::generate_pair;
88    use serde_json::json;
89
90    #[tokio::test]
91    async fn test_certify_basic() {
92        let authority = generate_pair().await.unwrap();
93        let alice = generate_pair().await.unwrap();
94        let cert = certify(&authority, std::slice::from_ref(&alice.pub_key), None)
95            .await
96            .unwrap();
97        let payload = verify_certificate(&cert, &authority.pub_key).unwrap();
98        assert!(is_certified(&payload, &alice.pub_key));
99    }
100
101    #[tokio::test]
102    async fn test_certify_with_policies() {
103        let authority = generate_pair().await.unwrap();
104        let alice = generate_pair().await.unwrap();
105        let policies = json!({"w": "skills/", "r": ".*"});
106        let cert = certify(
107            &authority,
108            std::slice::from_ref(&alice.pub_key),
109            Some(&policies),
110        )
111        .await
112        .unwrap();
113        let payload = verify_certificate(&cert, &authority.pub_key).unwrap();
114        assert_eq!(payload["w"].as_str(), Some("skills/"));
115        assert_eq!(payload["r"].as_str(), Some(".*"));
116    }
117
118    #[tokio::test]
119    async fn test_certify_expired() {
120        let authority = generate_pair().await.unwrap();
121        let alice = generate_pair().await.unwrap();
122        let policies = json!({"e": 1000.0_f64}); // 1970
123        let cert = certify(&authority, &[alice.pub_key], Some(&policies))
124            .await
125            .unwrap();
126        assert!(verify_certificate(&cert, &authority.pub_key).is_err());
127    }
128
129    #[tokio::test]
130    async fn test_certify_wrong_authority() {
131        let authority = generate_pair().await.unwrap();
132        let imposter = generate_pair().await.unwrap();
133        let alice = generate_pair().await.unwrap();
134        let cert = certify(&authority, &[alice.pub_key], None).await.unwrap();
135        assert!(verify_certificate(&cert, &imposter.pub_key).is_err());
136    }
137
138    #[tokio::test]
139    async fn test_certify_multiple_certificants() {
140        let authority = generate_pair().await.unwrap();
141        let alice = generate_pair().await.unwrap();
142        let bob = generate_pair().await.unwrap();
143        let cert = certify(
144            &authority,
145            &[alice.pub_key.clone(), bob.pub_key.clone()],
146            None,
147        )
148        .await
149        .unwrap();
150        let payload = verify_certificate(&cert, &authority.pub_key).unwrap();
151        assert!(is_certified(&payload, &alice.pub_key));
152        assert!(is_certified(&payload, &bob.pub_key));
153        assert!(!is_certified(&payload, "unknown"));
154    }
155
156    #[test]
157    fn test_is_certified_missing_c_field() {
158        let payload = json!({"other": "data"});
159        assert!(!is_certified(&payload, "anykey"));
160    }
161
162    #[test]
163    fn test_is_certified_empty_array() {
164        let payload = json!({"c": []});
165        assert!(!is_certified(&payload, "anykey"));
166    }
167}