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: Support generic hashers in UcanBuilder and ProofChain. #89

Merged
merged 1 commit into from
May 26, 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
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