diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs index d259a3f1..a06424e9 100644 --- a/ucan/src/builder.rs +++ b/ucan/src/builder.rs @@ -10,12 +10,11 @@ use crate::{ }; use anyhow::{anyhow, Result}; use base64::Engine; -use cid::Cid; +use cid::multihash::Code; use log::warn; use rand::Rng; use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; -use std::convert::TryFrom; /// A signable is a UCAN that has all the state it needs in order to be signed, /// but has not yet been signed. @@ -216,8 +215,10 @@ where /// Includes a UCAN in the list of proofs for the UCAN to be built. /// Note that the proof's audience must match this UCAN's issuer /// or else the proof chain will be invalidated! - pub fn witnessed_by(mut self, authority: &Ucan) -> Self { - match Cid::try_from(authority) { + /// The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()] + /// algorithm, unless one is provided. + pub fn witnessed_by(mut self, authority: &Ucan, hasher: Option) -> Self { + match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::::default_hasher())) { Ok(proof) => self.proofs.push(proof.to_string()), Err(error) => warn!("Failed to add authority to proofs: {}", error), } @@ -237,9 +238,11 @@ where } /// Delegate all capabilities from a given proof to the audience of the UCAN - /// you're building - pub fn delegating_from(mut self, authority: &Ucan) -> Self { - match Cid::try_from(authority) { + /// you're building. + /// The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()] + /// algorithm, unless one is provided. + pub fn delegating_from(mut self, authority: &Ucan, hasher: Option) -> Self { + match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::::default_hasher())) { Ok(proof) => { self.proofs.push(proof.to_string()); let proof_index = self.proofs.len() - 1; @@ -260,6 +263,11 @@ where self } + /// Returns the default hasher ([Code::Blake3_256]) used for [Cid] encodings. + pub fn default_hasher() -> Code { + Code::Blake3_256 + } + fn implied_expiration(&self) -> Option { if self.expiration.is_some() { self.expiration diff --git a/ucan/src/tests/attenuation.rs b/ucan/src/tests/attenuation.rs index 0b35c525..6b493e9a 100644 --- a/ucan/src/tests/attenuation.rs +++ b/ucan/src/tests/attenuation.rs @@ -40,7 +40,7 @@ pub async fn it_works_with_a_simple_example() { .issued_by(&identities.bob_key) .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan) + .witnessed_by(&leaf_ucan, None) .claiming_capability(&send_email_as_alice) .build() .unwrap() @@ -99,7 +99,7 @@ pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { .issued_by(&identities.bob_key) .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan) + .witnessed_by(&leaf_ucan, None) .claiming_capability(&send_email_as_bob) .build() .unwrap() @@ -172,8 +172,8 @@ pub async fn it_finds_the_right_proof_chain_for_the_originator() { .issued_by(&identities.mallory_key) .for_audience(identities.alice_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan_alice) - .witnessed_by(&leaf_ucan_bob) + .witnessed_by(&leaf_ucan_alice, None) + .witnessed_by(&leaf_ucan_bob, None) .claiming_capability(&send_email_as_alice) .claiming_capability(&send_email_as_bob) .build() @@ -262,8 +262,8 @@ pub async fn it_reports_all_chain_options() { .issued_by(&identities.mallory_key) .for_audience(identities.alice_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan_alice) - .witnessed_by(&leaf_ucan_bob) + .witnessed_by(&leaf_ucan_alice, None) + .witnessed_by(&leaf_ucan_bob, None) .claiming_capability(&send_email_as_alice) .build() .unwrap() diff --git a/ucan/src/tests/builder.rs b/ucan/src/tests/builder.rs index 13d5a5e1..9512ac5b 100644 --- a/ucan/src/tests/builder.rs +++ b/ucan/src/tests/builder.rs @@ -1,10 +1,16 @@ use crate::{ builder::UcanBuilder, capability::{CapabilityIpld, CapabilitySemantics}, - tests::fixtures::{EmailSemantics, Identities, WNFSSemantics}, + chain::ProofChain, + crypto::did::DidParser, + store::UcanJwtStore, + tests::fixtures::{ + Blake2bMemoryStore, EmailSemantics, Identities, WNFSSemantics, SUPPORTED_KEYS, + }, time::now, }; -use cid::Cid; +use cid::multihash::Code; +use did_key::PatchedKeyPair; use serde_json::json; #[cfg(target_arch = "wasm32")] @@ -121,7 +127,7 @@ async fn it_prevents_duplicate_proofs() { .issued_by(&identities.bob_key) .for_audience(identities.mallory_did.as_str()) .with_lifetime(30) - .witnessed_by(&ucan) + .witnessed_by(&ucan, None) .claiming_capability(&attenuated_cap_1) .claiming_capability(&attenuated_cap_2) .build() @@ -132,6 +138,55 @@ async fn it_prevents_duplicate_proofs() { assert_eq!( next_ucan.proofs(), - &Some(vec![Cid::try_from(ucan).unwrap().to_string()]) + &Some(vec![ucan + .to_cid(UcanBuilder::::default_hasher()) + .unwrap() + .to_string()]) ) } + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_can_use_custom_hasher() { + 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.alice_key) + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, Some(Code::Blake2b256)) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let mut store = Blake2bMemoryStore::default(); + + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let _ = store + .write_token(&delegated_token.encode().unwrap()) + .await + .unwrap(); + + let valid_chain = + ProofChain::from_ucan(delegated_token, Some(now()), &mut did_parser, &store).await; + + assert!(valid_chain.is_ok()); +} diff --git a/ucan/src/tests/chain.rs b/ucan/src/tests/chain.rs index 59ad76b4..d671b590 100644 --- a/ucan/src/tests/chain.rs +++ b/ucan/src/tests/chain.rs @@ -33,7 +33,7 @@ pub async fn it_decodes_deep_ucan_chains() { .issued_by(&identities.bob_key) .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan) + .witnessed_by(&leaf_ucan, None) .build() .unwrap() .sign() @@ -80,7 +80,7 @@ pub async fn it_fails_with_incorrect_chaining() { .issued_by(&identities.alice_key) .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan) + .witnessed_by(&leaf_ucan, None) .build() .unwrap() .sign() @@ -122,7 +122,7 @@ pub async fn it_can_be_instantiated_by_cid() { .issued_by(&identities.bob_key) .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan) + .witnessed_by(&leaf_ucan, None) .build() .unwrap() .sign() @@ -181,8 +181,8 @@ pub async fn it_can_handle_multiple_leaves() { .issued_by(&identities.bob_key) .for_audience(identities.alice_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan_1) - .witnessed_by(&leaf_ucan_2) + .witnessed_by(&leaf_ucan_1, None) + .witnessed_by(&leaf_ucan_2, None) .build() .unwrap() .sign() @@ -226,7 +226,7 @@ pub async fn it_can_use_a_custom_timestamp_to_validate_a_ucan() { .issued_by(&identities.bob_key) .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) - .witnessed_by(&leaf_ucan) + .witnessed_by(&leaf_ucan, None) .build() .unwrap() .sign() diff --git a/ucan/src/tests/fixtures/mod.rs b/ucan/src/tests/fixtures/mod.rs index 8965a9a6..82669dbd 100644 --- a/ucan/src/tests/fixtures/mod.rs +++ b/ucan/src/tests/fixtures/mod.rs @@ -1,7 +1,9 @@ mod capabilities; mod crypto; mod identities; +mod store; pub use capabilities::*; pub use crypto::*; pub use identities::*; +pub use store::*; diff --git a/ucan/src/tests/fixtures/store.rs b/ucan/src/tests/fixtures/store.rs new file mode 100644 index 00000000..bd158760 --- /dev/null +++ b/ucan/src/tests/fixtures/store.rs @@ -0,0 +1,48 @@ +use crate::store::{UcanStore, UcanStoreConditionalSend}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cid::{ + multihash::{Code, MultihashDigest}, + Cid, +}; +use libipld_core::{ + codec::{Codec, Decode, Encode}, + raw::RawCodec, +}; +use std::{ + collections::HashMap, + io::Cursor, + sync::{Arc, Mutex}, +}; + +#[derive(Clone, Default, Debug)] +pub struct Blake2bMemoryStore { + dags: Arc>>>, +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl UcanStore for Blake2bMemoryStore { + async fn read>(&self, cid: &Cid) -> Result> { + let dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; + + Ok(match dags.get(cid) { + Some(bytes) => Some(T::decode(RawCodec, &mut Cursor::new(bytes))?), + None => None, + }) + } + + async fn write + UcanStoreConditionalSend + core::fmt::Debug>( + &mut self, + token: T, + ) -> Result { + let codec = RawCodec; + let block = codec.encode(&token)?; + let cid = Cid::new_v1(codec.into(), Code::Blake2b256.digest(&block)); + + let mut dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; + dags.insert(cid, block); + + Ok(cid) + } +} diff --git a/ucan/src/tests/helpers.rs b/ucan/src/tests/helpers.rs index 0b857b1a..55f04ee9 100644 --- a/ucan/src/tests/helpers.rs +++ b/ucan/src/tests/helpers.rs @@ -47,8 +47,8 @@ pub async fn scaffold_ucan_builder(identities: &Identities) -> Result &str { &self.header.ucv } -} - -impl TryFrom<&Ucan> for Cid { - type Error = anyhow::Error; - fn try_from(value: &Ucan) -> Result { + pub fn to_cid(&self, hasher: Code) -> Result { let codec = RawCodec; - let token = value.encode()?; + let token = self.encode()?; let encoded = codec.encode(token.as_bytes())?; - - Ok(Cid::new_v1(codec.into(), Code::Blake3_256.digest(&encoded))) - } -} - -impl TryFrom for Cid { - type Error = anyhow::Error; - - fn try_from(value: Ucan) -> Result { - Cid::try_from(&value) + Ok(Cid::new_v1(codec.into(), hasher.digest(&encoded))) } }