From 51a934b304a3e921828d2de6517625541c60081e Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Wed, 24 May 2023 11:22:35 -0700 Subject: [PATCH] feat: Allow nullable expiry, per 0.9.0 spec. Fixes #23 --- ucan/src/builder.rs | 25 ++++++++--------- ucan/src/chain.rs | 2 +- ucan/src/ipld/ucan.rs | 2 +- ucan/src/tests/builder.rs | 5 ++-- ucan/src/tests/ucan.rs | 57 +++++++++++++++++++++++++++++++++++++++ ucan/src/ucan.rs | 18 ++++++++----- 6 files changed, 85 insertions(+), 24 deletions(-) diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs index a06424e9..b4ee3e1c 100644 --- a/ucan/src/builder.rs +++ b/ucan/src/builder.rs @@ -30,7 +30,7 @@ where pub capabilities: Vec, - pub expiration: u64, + pub expiration: Option, pub not_before: Option, pub facts: Vec, @@ -279,19 +279,16 @@ where pub fn build(self) -> Result> { match &self.issuer { Some(issuer) => match &self.audience { - Some(audience) => match self.implied_expiration() { - Some(expiration) => Ok(Signable { - issuer, - audience: audience.clone(), - not_before: self.not_before, - expiration, - facts: self.facts.clone(), - capabilities: self.capabilities.clone(), - proofs: self.proofs.clone(), - add_nonce: self.add_nonce, - }), - None => Err(anyhow!("Ambiguous lifetime")), - }, + Some(audience) => Ok(Signable { + issuer, + audience: audience.clone(), + not_before: self.not_before, + expiration: self.implied_expiration(), + facts: self.facts.clone(), + capabilities: self.capabilities.clone(), + proofs: self.proofs.clone(), + add_nonce: self.add_nonce, + }), None => Err(anyhow!("Missing audience")), }, None => Err(anyhow!("Missing issuer")), diff --git a/ucan/src/chain.rs b/ucan/src/chain.rs index dab6c458..8e8ecf73 100644 --- a/ucan/src/chain.rs +++ b/ucan/src/chain.rs @@ -18,7 +18,7 @@ const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSema pub struct CapabilityInfo { pub originators: BTreeSet, pub not_before: Option, - pub expires_at: u64, + pub expires_at: Option, pub capability: Capability, } diff --git a/ucan/src/ipld/ucan.rs b/ucan/src/ipld/ucan.rs index 2750284e..713b5264 100644 --- a/ucan/src/ipld/ucan.rs +++ b/ucan/src/ipld/ucan.rs @@ -20,7 +20,7 @@ pub struct UcanIpld { pub att: Vec, pub prf: Option>, - pub exp: u64, + pub exp: Option, pub fct: Option>, pub nnc: Option, diff --git a/ucan/src/tests/builder.rs b/ucan/src/tests/builder.rs index 9512ac5b..5488f826 100644 --- a/ucan/src/tests/builder.rs +++ b/ucan/src/tests/builder.rs @@ -64,7 +64,8 @@ async fn it_builds_with_a_simple_example() { assert_eq!(ucan.issuer(), identities.alice_did); assert_eq!(ucan.audience(), identities.bob_did); - assert_eq!(ucan.expires_at(), &expiration); + assert!(ucan.expires_at().is_some()); + assert_eq!(ucan.expires_at().unwrap(), expiration); assert!(ucan.not_before().is_some()); assert_eq!(ucan.not_before().unwrap(), not_before); assert_eq!(ucan.facts(), &Some(vec![fact_1, fact_2])); @@ -91,7 +92,7 @@ async fn it_builds_with_lifetime_in_seconds() { .await .unwrap(); - assert!(*ucan.expires_at() > (now() + 290)); + assert!(ucan.expires_at().unwrap() > (now() + 290)); } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] diff --git a/ucan/src/tests/ucan.rs b/ucan/src/tests/ucan.rs index f41f0def..83f1eea6 100644 --- a/ucan/src/tests/ucan.rs +++ b/ucan/src/tests/ucan.rs @@ -148,3 +148,60 @@ mod validate { assert!(ucan_a != ucan_c); } } + +mod spec_0_9_0 { + use crate::{builder::UcanBuilder, tests::fixtures::Identities}; + use anyhow::Result; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_allows_nullable_expiry() -> Result<()> { + let identities = Identities::new().await; + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .build()? + .sign() + .await?; + let other_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(2000) + .build()? + .sign() + .await?; + + assert_eq!(*ucan.expires_at(), None); + assert!(ucan.lifetime_ends_after(&other_ucan)); + assert!(!other_ucan.lifetime_ends_after(&ucan)); + + assert_eq!( + serde_json::to_value(ucan.clone())?, + serde_json::json!({ + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": crate::ucan::UCAN_VERSION + }, + "payload": { + "iss": ucan.issuer(), + "aud": ucan.audience(), + "exp": serde_json::Value::Null, + "att": [], + "fct": [], + "prf": [] + }, + "signed_data": ucan.signed_data(), + "signature": ucan.signature() + }) + ); + + Ok(()) + } +} diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs index c31899bc..c6e9f005 100644 --- a/ucan/src/ucan.rs +++ b/ucan/src/ucan.rs @@ -28,7 +28,7 @@ pub struct UcanHeader { pub struct UcanPayload { pub iss: String, pub aud: String, - pub exp: u64, + pub exp: Option, #[serde(skip_serializing_if = "Option::is_none")] pub nbf: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -99,9 +99,11 @@ impl Ucan { /// Returns true if the UCAN has past its expiration date pub fn is_expired(&self, now_time: Option) -> bool { - let now_time = now_time.unwrap_or_else(now); - - self.payload.exp < now_time + if let Some(exp) = self.payload.exp { + exp < now_time.unwrap_or_else(now) + } else { + false + } } /// Raw bytes of signed data for this UCAN @@ -135,7 +137,11 @@ impl Ucan { /// Returns true if this UCAN expires no earlier than the other pub fn lifetime_ends_after(&self, other: &Ucan) -> bool { - self.payload.exp >= other.payload.exp + match (self.payload.exp, other.payload.exp) { + (Some(exp), Some(other_exp)) => exp >= other_exp, + (Some(_), None) => false, + (None, _) => true, + } } /// Returns true if this UCAN's lifetime fully encompasses the other @@ -159,7 +165,7 @@ impl Ucan { &self.payload.prf } - pub fn expires_at(&self) -> &u64 { + pub fn expires_at(&self) -> &Option { &self.payload.exp }