From cda9d76bb67fb295784b1073001b23bffe5dc772 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 | 80 ++++++++++++++++++++++++++++++++++++--- ucan/src/ucan.rs | 18 ++++++--- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs index 9ab5c651..0a01daa5 100644 --- a/ucan/src/builder.rs +++ b/ucan/src/builder.rs @@ -31,7 +31,7 @@ where pub capabilities: Vec, - pub expiration: u64, + pub expiration: Option, pub not_before: Option, pub facts: FactsMap, @@ -282,19 +282,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 aa9f20a3..3c310337 100644 --- a/ucan/src/ipld/ucan.rs +++ b/ucan/src/ipld/ucan.rs @@ -19,7 +19,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 b8b368f3..e1b78a9c 100644 --- a/ucan/src/tests/builder.rs +++ b/ucan/src/tests/builder.rs @@ -66,7 +66,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!( @@ -99,7 +100,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 208e12fd..a80f2d25 100644 --- a/ucan/src/tests/ucan.rs +++ b/ucan/src/tests/ucan.rs @@ -6,6 +6,7 @@ mod validate { time::now, ucan::Ucan, }; + use anyhow::Result; use serde_json::json; #[cfg(target_arch = "wasm32")] @@ -75,7 +76,7 @@ mod validate { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_be_serialized_as_json() { + async fn it_can_be_serialized_as_json() -> Result<()> { let identities = Identities::new().await; let ucan = UcanBuilder::default() .issued_by(&identities.alice_key) @@ -83,13 +84,11 @@ mod validate { .not_before(now() / 1000) .with_lifetime(30) .with_fact("abc/challenge", json!({ "foo": "bar" })) - .build() - .unwrap() + .build()? .sign() - .await - .unwrap(); + .await?; - let ucan_json = serde_json::to_value(ucan.clone()).unwrap(); + let ucan_json = serde_json::to_value(ucan.clone())?; assert_eq!( ucan_json, @@ -113,6 +112,42 @@ mod validate { "signature": ucan.signature() }) ); + Ok(()) + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_can_be_serialized_as_json_without_optionals() -> 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 ucan_json = serde_json::to_value(ucan.clone())?; + + assert_eq!( + ucan_json, + serde_json::json!({ + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": crate::ucan::UCAN_VERSION, + "iss": ucan.issuer(), + "aud": ucan.audience(), + "exp": serde_json::Value::Null, + "att": [] + }, + "signed_data": ucan.signed_data(), + "signature": ucan.signature() + }) + ); + + Ok(()) } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] @@ -152,4 +187,37 @@ mod validate { assert!(ucan_a == ucan_b); assert!(ucan_a != ucan_c); } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn test_lifetime_ends_after() -> Result<()> { + let identities = Identities::new().await; + let forever_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .build()? + .sign() + .await?; + let early_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(2000) + .build()? + .sign() + .await?; + let later_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(4000) + .build()? + .sign() + .await?; + + assert_eq!(*forever_ucan.expires_at(), None); + assert!(forever_ucan.lifetime_ends_after(&early_ucan)); + assert!(!early_ucan.lifetime_ends_after(&forever_ucan)); + assert!(later_ucan.lifetime_ends_after(&early_ucan)); + + Ok(()) + } } diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs index e5ea3cf5..281cb6df 100644 --- a/ucan/src/ucan.rs +++ b/ucan/src/ucan.rs @@ -30,7 +30,7 @@ pub struct UcanPayload { pub ucv: String, 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")] @@ -101,9 +101,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 @@ -137,7 +139,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 @@ -161,7 +167,7 @@ impl Ucan { &self.payload.prf } - pub fn expires_at(&self) -> &u64 { + pub fn expires_at(&self) -> &Option { &self.payload.exp }