Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow nullable expiry per 0.9.0 spec. Fixes #23 #95

Merged
merged 1 commit into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
jsantell marked this conversation as resolved.
Show resolved Hide resolved
(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