Skip to content

Commit e4d0f76

Browse files
committed
feat: Allow nullable expiry, per 0.9.0 spec. Fixes #23
1 parent 806c646 commit e4d0f76

File tree

6 files changed

+64
-24
lines changed

6 files changed

+64
-24
lines changed

ucan/src/builder.rs

+11-14
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ where
3131

3232
pub capabilities: Vec<CapabilityIpld>,
3333

34-
pub expiration: u64,
34+
pub expiration: Option<u64>,
3535
pub not_before: Option<u64>,
3636

3737
pub facts: Vec<Value>,
@@ -259,19 +259,16 @@ where
259259
pub fn build(self) -> Result<Signable<'a, K>> {
260260
match &self.issuer {
261261
Some(issuer) => match &self.audience {
262-
Some(audience) => match self.implied_expiration() {
263-
Some(expiration) => Ok(Signable {
264-
issuer,
265-
audience: audience.clone(),
266-
not_before: self.not_before,
267-
expiration,
268-
facts: self.facts.clone(),
269-
capabilities: self.capabilities.clone(),
270-
proofs: self.proofs.clone(),
271-
add_nonce: self.add_nonce,
272-
}),
273-
None => Err(anyhow!("Ambiguous lifetime")),
274-
},
262+
Some(audience) => Ok(Signable {
263+
issuer,
264+
audience: audience.clone(),
265+
not_before: self.not_before,
266+
expiration: self.implied_expiration(),
267+
facts: self.facts.clone(),
268+
capabilities: self.capabilities.clone(),
269+
proofs: self.proofs.clone(),
270+
add_nonce: self.add_nonce,
271+
}),
275272
None => Err(anyhow!("Missing audience")),
276273
},
277274
None => Err(anyhow!("Missing issuer")),

ucan/src/chain.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSema
1818
pub struct CapabilityInfo<S: Scope, A: Action> {
1919
pub originators: BTreeSet<String>,
2020
pub not_before: Option<u64>,
21-
pub expires_at: u64,
21+
pub expires_at: Option<u64>,
2222
pub capability: Capability<S, A>,
2323
}
2424

ucan/src/ipld/ucan.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub struct UcanIpld {
2020

2121
pub att: Vec<CapabilityIpld>,
2222
pub prf: Vec<Cid>,
23-
pub exp: u64,
23+
pub exp: Option<u64>,
2424
pub fct: Vec<Value>,
2525

2626
pub nnc: Option<String>,

ucan/src/tests/builder.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ async fn it_builds_with_a_simple_example() {
5858

5959
assert_eq!(ucan.issuer(), identities.alice_did);
6060
assert_eq!(ucan.audience(), identities.bob_did);
61-
assert_eq!(ucan.expires_at(), &expiration);
61+
assert!(ucan.expires_at().is_some());
62+
assert_eq!(ucan.expires_at().unwrap(), expiration);
6263
assert!(ucan.not_before().is_some());
6364
assert_eq!(ucan.not_before().unwrap(), not_before);
6465
assert_eq!(ucan.facts(), &vec![fact_1, fact_2]);
@@ -85,7 +86,7 @@ async fn it_builds_with_lifetime_in_seconds() {
8586
.await
8687
.unwrap();
8788

88-
assert!(*ucan.expires_at() > (now() + 290));
89+
assert!(ucan.expires_at().unwrap() > (now() + 290));
8990
}
9091

9192
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]

ucan/src/tests/ucan.rs

+35
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,38 @@ mod validate {
112112
);
113113
}
114114
}
115+
116+
mod spec_0_9_0 {
117+
use crate::{builder::UcanBuilder, tests::fixtures::Identities};
118+
use anyhow::Result;
119+
120+
#[cfg(target_arch = "wasm32")]
121+
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
122+
123+
#[cfg(target_arch = "wasm32")]
124+
wasm_bindgen_test_configure!(run_in_browser);
125+
126+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
127+
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
128+
async fn it_allows_nullable_expiry() -> Result<()> {
129+
let identities = Identities::new().await;
130+
let ucan = UcanBuilder::default()
131+
.issued_by(&identities.alice_key)
132+
.for_audience(identities.bob_did.as_str())
133+
.build()?
134+
.sign()
135+
.await?;
136+
let other_ucan = UcanBuilder::default()
137+
.issued_by(&identities.alice_key)
138+
.for_audience(identities.bob_did.as_str())
139+
.with_lifetime(2000)
140+
.build()?
141+
.sign()
142+
.await?;
143+
144+
assert_eq!(*ucan.expires_at(), None);
145+
assert!(ucan.lifetime_ends_after(&other_ucan));
146+
assert!(!other_ucan.lifetime_ends_after(&ucan));
147+
Ok(())
148+
}
149+
}

ucan/src/ucan.rs

+13-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ pub struct UcanHeader {
2828
pub struct UcanPayload {
2929
pub iss: String,
3030
pub aud: String,
31-
pub exp: u64,
31+
#[serde(skip_serializing_if = "Option::is_none")]
32+
pub exp: Option<u64>,
3233
#[serde(skip_serializing_if = "Option::is_none")]
3334
pub nbf: Option<u64>,
3435
#[serde(skip_serializing_if = "Option::is_none")]
@@ -97,9 +98,11 @@ impl Ucan {
9798

9899
/// Returns true if the UCAN has past its expiration date
99100
pub fn is_expired(&self, now_time: Option<u64>) -> bool {
100-
let now_time = now_time.unwrap_or_else(now);
101-
102-
self.payload.exp < now_time
101+
if let Some(exp) = self.payload.exp {
102+
exp < now_time.unwrap_or_else(now)
103+
} else {
104+
false
105+
}
103106
}
104107

105108
/// Raw bytes of signed data for this UCAN
@@ -133,7 +136,11 @@ impl Ucan {
133136

134137
/// Returns true if this UCAN expires no earlier than the other
135138
pub fn lifetime_ends_after(&self, other: &Ucan) -> bool {
136-
self.payload.exp >= other.payload.exp
139+
match (self.payload.exp, other.payload.exp) {
140+
(Some(exp), Some(other_exp)) => exp >= other_exp,
141+
(Some(_), None) => false,
142+
(None, _) => true,
143+
}
137144
}
138145

139146
/// Returns true if this UCAN's lifetime fully encompasses the other
@@ -157,7 +164,7 @@ impl Ucan {
157164
&self.payload.prf
158165
}
159166

160-
pub fn expires_at(&self) -> &u64 {
167+
pub fn expires_at(&self) -> &Option<u64> {
161168
&self.payload.exp
162169
}
163170

0 commit comments

Comments
 (0)