Skip to content

Commit

Permalink
feat: Support generic hashers in UcanBuilder and ProofChain.
Browse files Browse the repository at this point in the history
* Add `UcanBuilder::with_hasher()` to provide a hasher when encoding
  proofs.
* Change `TryFrom<&Ucan> for Cid` into `Ucan::into_cid(hasher)`.
  • Loading branch information
jsantell committed May 22, 2023
1 parent 806c646 commit e880342
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 23 deletions.
24 changes: 20 additions & 4 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 @@ -113,6 +112,8 @@ where
facts: Vec<Value>,
proofs: Vec<String>,
add_nonce: bool,

hasher: Code,
}

impl<'a, K> Default for UcanBuilder<'a, K>
Expand Down Expand Up @@ -141,6 +142,8 @@ where
facts: Vec::new(),
proofs: Vec::new(),
add_nonce: false,

hasher: UcanBuilder::<K>::default_hasher(),
}
}
}
Expand Down Expand Up @@ -205,7 +208,7 @@ where
/// 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) {
match authority.to_cid(self.hasher) {
Ok(proof) => self.proofs.push(proof.to_string()),
Err(error) => warn!("Failed to add authority to proofs: {}", error),
}
Expand All @@ -227,7 +230,7 @@ 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) {
match authority.to_cid(self.hasher) {
Ok(proof) => {
self.proofs.push(proof.to_string());
let proof_index = self.proofs.len() - 1;
Expand All @@ -248,6 +251,19 @@ where
self
}

/// When encoding proofs as a [Cid], use `hasher`. Must be called before
/// methods that utilize the encoding, e.g. `delegating_from()` and
/// `witnessed_by()`. Uses `UcanBuilder::default_hasher()` by default.
pub fn with_hasher(mut self, hasher: Code) -> Self {
self.hasher = hasher;
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
63 changes: 60 additions & 3 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 @@ -132,6 +138,57 @@ async fn it_prevents_duplicate_proofs() {

assert_eq!(
next_ucan.proofs(),
&vec![Cid::try_from(ucan).unwrap().to_string()]
&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()
.with_hasher(Code::Blake2b256)
.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()
.with_hasher(Code::Blake2b256)
.issued_by(&identities.alice_key)
.issued_by(&identities.bob_key)
.for_audience(identities.mallory_did.as_str())
.with_lifetime(50)
.witnessed_by(&leaf_ucan)
.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());
}
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::*;
42 changes: 42 additions & 0 deletions ucan/src/tests/fixtures/store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::store::{UcanStore, UcanStoreConditionalSend};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use cid::multihash::{Code, MultihashDigest};
use cid::Cid;
use libipld_core::codec::{Codec, Decode, Encode};
use libipld_core::raw::RawCodec;
use std::collections::HashMap;
use std::io::Cursor;
use std::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)
}
}
19 changes: 3 additions & 16 deletions ucan/src/ucan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,25 +180,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 e880342

Please sign in to comment.