ucan/
ucan.rs

1use crate::{
2    capability::Capabilities,
3    crypto::did::DidParser,
4    serde::{Base64Encode, DagJson},
5    time::now,
6};
7use anyhow::{anyhow, Result};
8use base64::Engine;
9use cid::{
10    multihash::{Code, MultihashDigest},
11    Cid,
12};
13use libipld_core::{codec::Codec, raw::RawCodec};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::{collections::BTreeMap, convert::TryFrom, str::FromStr};
17
18pub const UCAN_VERSION: &str = "0.10.0-canary";
19
20pub type FactsMap = BTreeMap<String, Value>;
21
22#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
23pub struct UcanHeader {
24    pub alg: String,
25    pub typ: String,
26}
27
28#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
29pub struct UcanPayload {
30    pub ucv: String,
31    pub iss: String,
32    pub aud: String,
33    pub exp: Option<u64>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub nbf: Option<u64>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub nnc: Option<String>,
38    pub cap: Capabilities,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub fct: Option<FactsMap>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub prf: Option<Vec<String>>,
43}
44
45#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
46pub struct Ucan {
47    header: UcanHeader,
48    payload: UcanPayload,
49    signed_data: Vec<u8>,
50    signature: Vec<u8>,
51}
52
53impl Ucan {
54    pub fn new(
55        header: UcanHeader,
56        payload: UcanPayload,
57        signed_data: Vec<u8>,
58        signature: Vec<u8>,
59    ) -> Self {
60        Ucan {
61            signed_data,
62            header,
63            payload,
64            signature,
65        }
66    }
67
68    /// Validate the UCAN's signature and timestamps
69    pub async fn validate<'a>(
70        &self,
71        now_time: Option<u64>,
72        did_parser: &mut DidParser,
73    ) -> Result<()> {
74        if self.is_expired(now_time) {
75            return Err(anyhow!("Expired"));
76        }
77
78        if self.is_too_early() {
79            return Err(anyhow!("Not active yet (too early)"));
80        }
81
82        self.check_signature(did_parser).await
83    }
84
85    /// Validate that the signed data was signed by the stated issuer
86    pub async fn check_signature<'a>(&self, did_parser: &mut DidParser) -> Result<()> {
87        let key = did_parser.parse(&self.payload.iss)?;
88        key.verify(&self.signed_data, &self.signature).await
89    }
90
91    /// Produce a base64-encoded serialization of the UCAN suitable for
92    /// transferring in a header field
93    pub fn encode(&self) -> Result<String> {
94        let header = self.header.jwt_base64_encode()?;
95        let payload = self.payload.jwt_base64_encode()?;
96        let signature =
97            base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.signature.as_slice());
98
99        Ok(format!("{header}.{payload}.{signature}"))
100    }
101
102    /// Returns true if the UCAN has past its expiration date
103    pub fn is_expired(&self, now_time: Option<u64>) -> bool {
104        if let Some(exp) = self.payload.exp {
105            exp < now_time.unwrap_or_else(now)
106        } else {
107            false
108        }
109    }
110
111    /// Raw bytes of signed data for this UCAN
112    pub fn signed_data(&self) -> &[u8] {
113        &self.signed_data
114    }
115
116    pub fn signature(&self) -> &[u8] {
117        &self.signature
118    }
119
120    /// Returns true if the not-before ("nbf") time is still in the future
121    pub fn is_too_early(&self) -> bool {
122        match self.payload.nbf {
123            Some(nbf) => nbf > now(),
124            None => false,
125        }
126    }
127
128    /// Returns true if this UCAN's lifetime begins no later than the other
129    /// Note that if a UCAN specifies an NBF but the other does not, the
130    /// other has an unbounded start time and this function will return
131    /// false.
132    pub fn lifetime_begins_before(&self, other: &Ucan) -> bool {
133        match (self.payload.nbf, other.payload.nbf) {
134            (Some(nbf), Some(other_nbf)) => nbf <= other_nbf,
135            (Some(_), None) => false,
136            _ => true,
137        }
138    }
139
140    /// Returns true if this UCAN expires no earlier than the other
141    pub fn lifetime_ends_after(&self, other: &Ucan) -> bool {
142        match (self.payload.exp, other.payload.exp) {
143            (Some(exp), Some(other_exp)) => exp >= other_exp,
144            (Some(_), None) => false,
145            (None, _) => true,
146        }
147    }
148
149    /// Returns true if this UCAN's lifetime fully encompasses the other
150    pub fn lifetime_encompasses(&self, other: &Ucan) -> bool {
151        self.lifetime_begins_before(other) && self.lifetime_ends_after(other)
152    }
153
154    pub fn algorithm(&self) -> &str {
155        &self.header.alg
156    }
157
158    pub fn issuer(&self) -> &str {
159        &self.payload.iss
160    }
161
162    pub fn audience(&self) -> &str {
163        &self.payload.aud
164    }
165
166    pub fn proofs(&self) -> &Option<Vec<String>> {
167        &self.payload.prf
168    }
169
170    pub fn expires_at(&self) -> &Option<u64> {
171        &self.payload.exp
172    }
173
174    pub fn not_before(&self) -> &Option<u64> {
175        &self.payload.nbf
176    }
177
178    pub fn nonce(&self) -> &Option<String> {
179        &self.payload.nnc
180    }
181
182    #[deprecated(since = "0.4.0", note = "use `capabilities()`")]
183    pub fn attenuation(&self) -> &Capabilities {
184        self.capabilities()
185    }
186
187    pub fn capabilities(&self) -> &Capabilities {
188        &self.payload.cap
189    }
190
191    pub fn facts(&self) -> &Option<FactsMap> {
192        &self.payload.fct
193    }
194
195    pub fn version(&self) -> &str {
196        &self.payload.ucv
197    }
198
199    pub fn to_cid(&self, hasher: Code) -> Result<Cid> {
200        let codec = RawCodec;
201        let token = self.encode()?;
202        let encoded = codec.encode(token.as_bytes())?;
203        Ok(Cid::new_v1(codec.into(), hasher.digest(&encoded)))
204    }
205}
206
207/// Deserialize an encoded UCAN token string reference into a UCAN
208impl<'a> TryFrom<&'a str> for Ucan {
209    type Error = anyhow::Error;
210
211    fn try_from(ucan_token: &str) -> Result<Self, Self::Error> {
212        Ucan::from_str(ucan_token)
213    }
214}
215
216/// Deserialize an encoded UCAN token string into a UCAN
217impl TryFrom<String> for Ucan {
218    type Error = anyhow::Error;
219
220    fn try_from(ucan_token: String) -> Result<Self, Self::Error> {
221        Ucan::from_str(ucan_token.as_str())
222    }
223}
224
225/// Deserialize an encoded UCAN token string reference into a UCAN
226impl FromStr for Ucan {
227    type Err = anyhow::Error;
228
229    fn from_str(ucan_token: &str) -> Result<Self, Self::Err> {
230        // better to create multiple iterators than collect, or clone.
231        let signed_data = ucan_token
232            .split('.')
233            .take(2)
234            .map(String::from)
235            .reduce(|l, r| format!("{l}.{r}"))
236            .ok_or_else(|| anyhow!("Could not parse signed data from token string"))?;
237
238        let mut parts = ucan_token.split('.').map(|str| {
239            base64::engine::general_purpose::URL_SAFE_NO_PAD
240                .decode(str)
241                .map_err(|error| anyhow!(error))
242        });
243
244        let header = parts
245            .next()
246            .ok_or_else(|| anyhow!("Missing UCAN header in token part"))?
247            .map(|decoded| UcanHeader::from_dag_json(&decoded))
248            .map_err(|e| e.context("Could not decode UCAN header base64"))?
249            .map_err(|e| e.context("Could not parse UCAN header JSON"))?;
250
251        let payload = parts
252            .next()
253            .ok_or_else(|| anyhow!("Missing UCAN payload in token part"))?
254            .map(|decoded| UcanPayload::from_dag_json(&decoded))
255            .map_err(|e| e.context("Could not decode UCAN payload base64"))?
256            .map_err(|e| e.context("Could not parse UCAN payload JSON"))?;
257
258        let signature = parts
259            .next()
260            .ok_or_else(|| anyhow!("Missing UCAN signature in token part"))?
261            .map_err(|e| e.context("Could not parse UCAN signature base64"))?;
262
263        Ok(Ucan::new(
264            header,
265            payload,
266            signed_data.as_bytes().into(),
267            signature,
268        ))
269    }
270}