From 1732a8911b67546f446126e4d469126f61769b44 Mon Sep 17 00:00:00 2001 From: Christopher Joel <240083+cdata@users.noreply.github.com> Date: Thu, 4 May 2023 13:01:45 -0700 Subject: [PATCH] feat!: Custom 'now' for proof chain validation (#83) --- ucan/src/chain.rs | 24 ++++++++++++--- ucan/src/lib.rs | 2 +- ucan/src/tests/attenuation.rs | 15 ++++----- ucan/src/tests/chain.rs | 57 ++++++++++++++++++++++++++++++++--- ucan/src/tests/ucan.rs | 2 +- ucan/src/ucan.rs | 14 ++++++--- 6 files changed, 92 insertions(+), 22 deletions(-) diff --git a/ucan/src/chain.rs b/ucan/src/chain.rs index c26e63d5..afe48d65 100644 --- a/ucan/src/chain.rs +++ b/ucan/src/chain.rs @@ -51,20 +51,22 @@ impl ProofChain { #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] pub async fn from_ucan( ucan: Ucan, + now_time: Option, did_parser: &mut DidParser, store: &S, ) -> Result where S: UcanJwtStore, { - ucan.validate(did_parser).await?; + ucan.validate(now_time, did_parser).await?; let mut proofs: Vec = Vec::new(); for cid_string in ucan.proofs().iter() { let cid = Cid::try_from(cid_string.as_str())?; let ucan_token = store.require_token(&cid).await?; - let proof_chain = Self::try_from_token_string(&ucan_token, did_parser, store).await?; + let proof_chain = + Self::try_from_token_string(&ucan_token, now_time, did_parser, store).await?; proof_chain.validate_link_to(&ucan)?; proofs.push(proof_chain); } @@ -105,16 +107,28 @@ impl ProofChain { /// Instantiate a [ProofChain] from a [Cid], given a [UcanJwtStore] and [DidParser] /// The [Cid] must resolve to a JWT token string - pub async fn from_cid(cid: &Cid, did_parser: &mut DidParser, store: &S) -> Result + pub async fn from_cid( + cid: &Cid, + now_time: Option, + did_parser: &mut DidParser, + store: &S, + ) -> Result where S: UcanJwtStore, { - Self::try_from_token_string(&store.require_token(cid).await?, did_parser, store).await + Self::try_from_token_string( + &store.require_token(cid).await?, + now_time, + did_parser, + store, + ) + .await } /// Instantiate a [ProofChain] from a JWT token string, given a [UcanJwtStore] and [DidParser] pub async fn try_from_token_string<'a, S>( ucan_token_string: &str, + now_time: Option, did_parser: &mut DidParser, store: &S, ) -> Result @@ -122,7 +136,7 @@ impl ProofChain { S: UcanJwtStore, { let ucan = Ucan::try_from(ucan_token_string)?; - Self::from_ucan(ucan, did_parser, store).await + Self::from_ucan(ucan, now_time, did_parser, store).await } fn validate_link_to(&self, ucan: &Ucan) -> Result<()> { diff --git a/ucan/src/lib.rs b/ucan/src/lib.rs index 35ac2fea..b020bf79 100644 --- a/ucan/src/lib.rs +++ b/ucan/src/lib.rs @@ -61,7 +61,7 @@ //! { //! let mut did_parser = DidParser::new(SUPPORTED_KEY_TYPES); //! -//! Ok(ProofChain::try_from_token_string(ucan_token, &mut did_parser, store).await? +//! Ok(ProofChain::try_from_token_string(ucan_token, None, &mut did_parser, store).await? //! .reduce_capabilities(semantics)) //! } //! ``` diff --git a/ucan/src/tests/attenuation.rs b/ucan/src/tests/attenuation.rs index a4d58852..0b35c525 100644 --- a/ucan/src/tests/attenuation.rs +++ b/ucan/src/tests/attenuation.rs @@ -57,7 +57,7 @@ pub async fn it_works_with_a_simple_example() { .unwrap(); let chain = - ProofChain::try_from_token_string(attenuated_token.as_str(), &mut did_parser, &store) + ProofChain::try_from_token_string(attenuated_token.as_str(), None, &mut did_parser, &store) .await .unwrap(); @@ -115,10 +115,11 @@ pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { .await .unwrap(); - let capability_infos = ProofChain::try_from_token_string(&ucan_token, &mut did_parser, &store) - .await - .unwrap() - .reduce_capabilities(&email_semantics); + let capability_infos = + ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) + .await + .unwrap() + .reduce_capabilities(&email_semantics); assert_eq!(capability_infos.len(), 1); @@ -193,7 +194,7 @@ pub async fn it_finds_the_right_proof_chain_for_the_originator() { .await .unwrap(); - let proof_chain = ProofChain::try_from_token_string(&ucan_token, &mut did_parser, &store) + let proof_chain = ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) .await .unwrap(); let capability_infos = proof_chain.reduce_capabilities(&email_semantics); @@ -282,7 +283,7 @@ pub async fn it_reports_all_chain_options() { .await .unwrap(); - let proof_chain = ProofChain::try_from_token_string(&ucan_token, &mut did_parser, &store) + let proof_chain = ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) .await .unwrap(); let capability_infos = proof_chain.reduce_capabilities(&email_semantics); diff --git a/ucan/src/tests/chain.rs b/ucan/src/tests/chain.rs index ddb616d6..59ad76b4 100644 --- a/ucan/src/tests/chain.rs +++ b/ucan/src/tests/chain.rs @@ -4,6 +4,7 @@ use crate::{ chain::ProofChain, crypto::did::DidParser, store::{MemoryStore, UcanJwtStore}, + time::now, }; #[cfg(target_arch = "wasm32")] @@ -48,7 +49,7 @@ pub async fn it_decodes_deep_ucan_chains() { .unwrap(); let chain = - ProofChain::try_from_token_string(delegated_token.as_str(), &mut did_parser, &store) + ProofChain::try_from_token_string(delegated_token.as_str(), None, &mut did_parser, &store) .await .unwrap(); @@ -95,7 +96,8 @@ pub async fn it_fails_with_incorrect_chaining() { .unwrap(); let parse_token_result = - ProofChain::try_from_token_string(delegated_token.as_str(), &mut did_parser, &store).await; + ProofChain::try_from_token_string(delegated_token.as_str(), None, &mut did_parser, &store) + .await; assert!(parse_token_result.is_err()); } @@ -138,7 +140,7 @@ pub async fn it_can_be_instantiated_by_cid() { let cid = store.write_token(&delegated_token).await.unwrap(); - let chain = ProofChain::from_cid(&cid, &mut did_parser, &store) + let chain = ProofChain::from_cid(&cid, None, &mut did_parser, &store) .await .unwrap(); @@ -199,7 +201,54 @@ pub async fn it_can_handle_multiple_leaves() { .await .unwrap(); - ProofChain::try_from_token_string(&delegated_token, &mut did_parser, &store) + ProofChain::try_from_token_string(&delegated_token, None, &mut did_parser, &store) .await .unwrap(); } + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_can_use_a_custom_timestamp_to_validate_a_ucan() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let delegated_token = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let cid = store.write_token(&delegated_token).await.unwrap(); + + let valid_chain = ProofChain::from_cid(&cid, Some(now()), &mut did_parser, &store).await; + + assert!(valid_chain.is_ok()); + + let invalid_chain = ProofChain::from_cid(&cid, Some(now() + 61), &mut did_parser, &store).await; + + assert!(invalid_chain.is_err()); +} diff --git a/ucan/src/tests/ucan.rs b/ucan/src/tests/ucan.rs index 9c02148b..1097c277 100644 --- a/ucan/src/tests/ucan.rs +++ b/ucan/src/tests/ucan.rs @@ -32,7 +32,7 @@ mod validate { let encoded_ucan = ucan.encode().unwrap(); let decoded_ucan = Ucan::try_from(encoded_ucan.as_str()).unwrap(); - decoded_ucan.validate(&mut did_parser).await.unwrap(); + decoded_ucan.validate(None, &mut did_parser).await.unwrap(); } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs index 623807ff..ed233742 100644 --- a/ucan/src/ucan.rs +++ b/ucan/src/ucan.rs @@ -62,8 +62,12 @@ impl Ucan { } /// Validate the UCAN's signature and timestamps - pub async fn validate<'a>(&self, did_parser: &mut DidParser) -> Result<()> { - if self.is_expired() { + pub async fn validate<'a>( + &self, + now_time: Option, + did_parser: &mut DidParser, + ) -> Result<()> { + if self.is_expired(now_time) { return Err(anyhow!("Expired")); } @@ -92,8 +96,10 @@ impl Ucan { } /// Returns true if the UCAN has past its expiration date - pub fn is_expired(&self) -> bool { - self.payload.exp < now() + pub fn is_expired(&self, now_time: Option) -> bool { + let now_time = now_time.unwrap_or_else(now); + + self.payload.exp < now_time } /// Raw bytes of signed data for this UCAN