Skip to content

Commit

Permalink
feat: Allow nullable expiry, per 0.9.0 spec. Fixes #23 (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsantell committed Jun 6, 2023
1 parent ae19741 commit 12d4756
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 30 deletions.
25 changes: 11 additions & 14 deletions ucan/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ where

pub capabilities: Vec<CapabilityIpld>,

pub expiration: u64,
pub expiration: Option<u64>,
pub not_before: Option<u64>,

pub facts: FactsMap,
Expand Down Expand Up @@ -282,19 +282,16 @@ where
pub fn build(self) -> Result<Signable<'a, K>> {
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")),
Expand Down
2 changes: 1 addition & 1 deletion ucan/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSema
pub struct CapabilityInfo<S: Scope, A: Action> {
pub originators: BTreeSet<String>,
pub not_before: Option<u64>,
pub expires_at: u64,
pub expires_at: Option<u64>,
pub capability: Capability<S, A>,
}

Expand Down
2 changes: 1 addition & 1 deletion ucan/src/ipld/ucan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub struct UcanIpld {

pub att: Vec<CapabilityIpld>,
pub prf: Option<Vec<Cid>>,
pub exp: u64,
pub exp: Option<u64>,
pub fct: Option<FactsMap>,

pub nnc: Option<String>,
Expand Down
5 changes: 3 additions & 2 deletions ucan/src/tests/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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)]
Expand Down
80 changes: 74 additions & 6 deletions ucan/src/tests/ucan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod validate {
time::now,
ucan::Ucan,
};
use anyhow::Result;

use serde_json::json;
#[cfg(target_arch = "wasm32")]
Expand Down Expand Up @@ -75,21 +76,19 @@ 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)
.for_audience(identities.bob_did.as_str())
.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,
Expand All @@ -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)]
Expand Down Expand Up @@ -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(())
}
}
18 changes: 12 additions & 6 deletions ucan/src/ucan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub struct UcanPayload {
pub ucv: String,
pub iss: String,
pub aud: String,
pub exp: u64,
pub exp: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -101,9 +101,11 @@ impl Ucan {

/// Returns true if the UCAN has past its expiration date
pub fn is_expired(&self, now_time: Option<u64>) -> 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
Expand Down Expand Up @@ -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
Expand All @@ -161,7 +167,7 @@ impl Ucan {
&self.payload.prf
}

pub fn expires_at(&self) -> &u64 {
pub fn expires_at(&self) -> &Option<u64> {
&self.payload.exp
}

Expand Down

0 comments on commit 12d4756

Please sign in to comment.