noosphere_core/data/
authority.rs

1use anyhow::Result;
2use cid::Cid;
3use libipld_cbor::DagCborCodec;
4use std::{hash::Hash, str::FromStr};
5use ucan::{crypto::KeyMaterial, store::UcanJwtStore, Ucan};
6
7use noosphere_storage::{base64_decode, base64_encode, BlockStore, UcanStore};
8use serde::{Deserialize, Serialize};
9
10use super::{DelegationsIpld, Link, RevocationsIpld};
11
12#[cfg(docs)]
13use crate::data::SphereIpld;
14
15/// A subdomain of a [SphereIpld] that pertains to the delegated authority to
16/// access a sphere, as well as the revocations of that authority.
17#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
18pub struct AuthorityIpld {
19    /// A pointer to the [DelegationsIpld] for this [AuthorityIpld], embodying
20    /// all authorizations for keys that operate on this sphere
21    pub delegations: Link<DelegationsIpld>,
22    /// A pointer to the [RevocationsIpld] for this [AuthorityIpld], embodying
23    /// revocations for any otherwise valid authorizations for keys issued by this
24    /// sphere in the past.
25    pub revocations: Link<RevocationsIpld>,
26}
27
28impl AuthorityIpld {
29    /// Initialize an empty [AuthorityIpld], with valid [Cid]s referring to
30    /// empty [DelegationsIpld] and [RevocationsIpld] that are persisted in the
31    /// provided storage
32    pub async fn empty<S: BlockStore>(store: &mut S) -> Result<Self> {
33        let delegations_ipld = DelegationsIpld::empty(store).await?;
34        let delegations = store
35            .save::<DagCborCodec, _>(delegations_ipld)
36            .await?
37            .into();
38        let revocations_ipld = RevocationsIpld::empty(store).await?;
39        let revocations = store
40            .save::<DagCborCodec, _>(revocations_ipld)
41            .await?
42            .into();
43
44        Ok(AuthorityIpld {
45            delegations,
46            revocations,
47        })
48    }
49}
50
51#[cfg(doc)]
52use crate::data::Jwt;
53
54/// This delegation represents the sharing of access to resources within a
55/// sphere. The name of the delegation is for display purposes only, and helps
56/// the user identify the client device or application that the delegation is
57/// intended for.
58#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize, Hash)]
59pub struct DelegationIpld {
60    /// The human-readable name of the delegation
61    pub name: String,
62    /// A pointer to the [Jwt] created for this [DelegationIpld]
63    pub jwt: Cid,
64}
65
66impl DelegationIpld {
67    /// Stores a [Ucan] that delegates authority to a key, and initializes a
68    /// [DelegationIpld] for it that is appropriate for storing in a sphere
69    pub async fn register<S: BlockStore>(name: &str, jwt: &str, store: &S) -> Result<Self> {
70        let mut store = UcanStore(store.clone());
71        let cid = store.write_token(jwt).await?;
72
73        Ok(DelegationIpld {
74            name: name.to_string(),
75            jwt: cid,
76        })
77    }
78
79    /// Resolve a [Ucan] from storage via the pointer to a [Jwt] in this
80    /// [DelegationIpld]
81    pub async fn resolve_ucan<S: BlockStore>(&self, store: &S) -> Result<Ucan> {
82        let store = UcanStore(store.clone());
83        let jwt = store.require_token(&self.jwt).await?;
84
85        Ucan::from_str(&jwt)
86    }
87}
88
89/// See <https://github.com/ucan-wg/spec#66-revocation>
90/// TODO(ucan-wg/spec#112): Verify the form of this
91#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)]
92pub struct RevocationIpld {
93    /// Issuer's DID
94    pub iss: String,
95    /// JWT CID of the revoked UCAN (provisionally encoded as base64 URL-safe
96    /// string)
97    pub revoke: String,
98    /// Issuer's signature of "REVOKE:{jwt_cid}", provisionally encoded
99    /// as unpadded base64 URL-safe string
100    pub challenge: String,
101}
102
103impl RevocationIpld {
104    /// Revoke a delegation by the [Cid] of its associated [Jwt], using a key credential
105    /// of an authorizing ancestor of the original delegation
106    pub async fn revoke<K: KeyMaterial>(cid: &Cid, issuer: &K) -> Result<Self> {
107        Ok(RevocationIpld {
108            iss: issuer.get_did().await?,
109            revoke: cid.to_string(),
110            challenge: base64_encode(&issuer.sign(&Self::make_challenge_payload(cid)).await?)?,
111        })
112    }
113
114    /// Verify that the [RevocationIpld] is valid compared to the public key of
115    /// the issuer
116    pub async fn verify<K: KeyMaterial + ?Sized>(&self, claimed_issuer: &K) -> Result<()> {
117        let cid = Cid::try_from(self.revoke.as_str())?;
118        let challenge_payload = Self::make_challenge_payload(&cid);
119        let signature = base64_decode(&self.challenge)?;
120
121        claimed_issuer
122            .verify(&challenge_payload, &signature)
123            .await?;
124
125        Ok(())
126    }
127
128    fn make_challenge_payload(cid: &Cid) -> Vec<u8> {
129        format!("REVOKE:{cid}").as_bytes().to_vec()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use noosphere_storage::{MemoryStore, UcanStore};
136    use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore};
137
138    use crate::authority::generate_ed25519_key;
139
140    use super::{DelegationIpld, RevocationIpld};
141
142    #[cfg(target_arch = "wasm32")]
143    use wasm_bindgen_test::wasm_bindgen_test;
144
145    #[cfg(target_arch = "wasm32")]
146    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
147
148    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
149    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
150    async fn it_stores_a_registerd_jwt() {
151        let store = MemoryStore::default();
152        let key = generate_ed25519_key();
153
154        let ucan_jwt = UcanBuilder::default()
155            .issued_by(&key)
156            .for_audience(&key.get_did().await.unwrap())
157            .with_lifetime(128)
158            .build()
159            .unwrap()
160            .sign()
161            .await
162            .unwrap()
163            .encode()
164            .unwrap();
165
166        let delegation = DelegationIpld::register("foobar", &ucan_jwt, &store)
167            .await
168            .unwrap();
169
170        assert_eq!(
171            UcanStore(store).read_token(&delegation.jwt).await.unwrap(),
172            Some(ucan_jwt)
173        );
174    }
175
176    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
177    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
178    async fn it_can_verify_that_a_key_issued_a_revocation() {
179        let store = MemoryStore::default();
180        let key = generate_ed25519_key();
181        let other_key = generate_ed25519_key();
182
183        let ucan_jwt = UcanBuilder::default()
184            .issued_by(&key)
185            .for_audience(&key.get_did().await.unwrap())
186            .with_lifetime(128)
187            .build()
188            .unwrap()
189            .sign()
190            .await
191            .unwrap()
192            .encode()
193            .unwrap();
194
195        let delegation = DelegationIpld::register("foobar", &ucan_jwt, &store)
196            .await
197            .unwrap();
198
199        let revocation = RevocationIpld::revoke(&delegation.jwt, &key).await.unwrap();
200
201        assert!(revocation.verify(&key).await.is_ok());
202        assert!(revocation.verify(&other_key).await.is_err());
203    }
204}