Skip to content

Commit

Permalink
feat: Support generic hashers in UcanBuilder and ProofChain.
Browse files Browse the repository at this point in the history
  • Loading branch information
jsantell committed May 26, 2023
1 parent 6509170 commit 2aa5359
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 41 deletions.
22 changes: 15 additions & 7 deletions ucan/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Code>) -> Self {
match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::<K>::default_hasher())) {
Ok(proof) => self.proofs.push(proof.to_string()),
Err(error) => warn!("Failed to add authority to proofs: {}", error),
}
Expand All @@ -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<Code>) -> Self {
match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::<K>::default_hasher())) {
Ok(proof) => {
self.proofs.push(proof.to_string());
let proof_index = self.proofs.len() - 1;
Expand All @@ -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<u64> {
if self.expiration.is_some() {
self.expiration
Expand Down
12 changes: 6 additions & 6 deletions ucan/src/tests/attenuation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
63 changes: 59 additions & 4 deletions ucan/src/tests/builder.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -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()
Expand All @@ -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::<PatchedKeyPair>::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());
}
12 changes: 6 additions & 6 deletions ucan/src/tests/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions ucan/src/tests/fixtures/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod capabilities;
mod crypto;
mod identities;
mod store;

pub use capabilities::*;
pub use crypto::*;
pub use identities::*;
pub use store::*;
48 changes: 48 additions & 0 deletions ucan/src/tests/fixtures/store.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<HashMap<Cid, Vec<u8>>>>,
}

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl UcanStore<RawCodec> for Blake2bMemoryStore {
async fn read<T: Decode<RawCodec>>(&self, cid: &Cid) -> Result<Option<T>> {
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<T: Encode<RawCodec> + UcanStoreConditionalSend + core::fmt::Debug>(
&mut self,
token: T,
) -> Result<Cid> {
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)
}
}
4 changes: 2 additions & 2 deletions ucan/src/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ pub async fn scaffold_ucan_builder(identities: &Identities) -> Result<UcanBuilde
.issued_by(&identities.mallory_key)
.for_audience(identities.alice_did.as_str())
.with_expiration(1664232146010)
.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);

Expand Down
19 changes: 3 additions & 16 deletions ucan/src/ucan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,25 +182,12 @@ impl Ucan {
pub fn version(&self) -> &str {
&self.header.ucv
}
}

impl TryFrom<&Ucan> for Cid {
type Error = anyhow::Error;

fn try_from(value: &Ucan) -> Result<Self, Self::Error> {
pub fn to_cid(&self, hasher: Code) -> Result<Cid> {
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<Ucan> for Cid {
type Error = anyhow::Error;

fn try_from(value: Ucan) -> Result<Self, Self::Error> {
Cid::try_from(&value)
Ok(Cid::new_v1(codec.into(), hasher.digest(&encoded)))
}
}

Expand Down

0 comments on commit 2aa5359

Please sign in to comment.