diff --git a/ucan-key-support/Cargo.toml b/ucan-key-support/Cargo.toml index 0f83f4ad..6815b25a 100644 --- a/ucan-key-support/Cargo.toml +++ b/ucan-key-support/Cargo.toml @@ -20,22 +20,22 @@ version = "0.7.0-alpha.1" default = [] [dependencies] -ucan = {path = "../ucan", version = "0.7.0-alpha.1" } anyhow = "1.0.52" async-trait = "0.1.52" +bs58 = "0.4" ed25519-zebra = "^3" +log = "0.4" rsa = "0.6" sha2 = "0.10" -bs58 = "0.4" -log = "0.4" +ucan = { path = "../ucan", version = "0.7.0-alpha.1" } [build-dependencies] npm_rs = "0.2.1" [dev-dependencies] -rand = "~0.8" # NOTE: This is needed so that rand can be included in WASM builds getrandom = { version = "~0.2", features = ["js"] } +rand = "~0.8" wasm-bindgen-test = "~0.3" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/ucan/Cargo.toml b/ucan/Cargo.toml index d55de0d3..8f4fa839 100644 --- a/ucan/Cargo.toml +++ b/ucan/Cargo.toml @@ -20,33 +20,32 @@ edition = "2021" default = [] [dependencies] -cid = "~0.8" -anyhow = "^1" -async-trait = "~0.1" -async-recursion = "^1" -async-std = "^1" -serde_json = "^1" -serde = { version = "^1", features = ["derive"] } -base64 = "~0.13" -log = "~0.4" -url = "^2" -bs58 = "~0.4" -unsigned-varint = "~0.7" -libipld-core = { version = "~0.14", features = ["serde-codec", "serde"] } -libipld-json = "~0.14" -strum = "~0.24" -strum_macros = "~0.24" +anyhow = "1.0" +async-std = "1.0" +async-trait = "0.1" +base64 = "0.13" +bs58 = "0.4" +cid = "0.8" instant = { version = "0.1", features = ["wasm-bindgen", "stdweb"] } -rand = "~0.8" +libipld-core = { version = "0.14", features = ["serde-codec", "serde"] } +libipld-json = "0.14" +log = "0.4" +rand = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +strum = "0.24" +strum_macros = "0.24" +unsigned-varint = "0.7" +url = "2.0" [target.'cfg(target_arch = "wasm32")'.dependencies] # NOTE: This is needed so that rand can be included in WASM builds -getrandom = { version = "~0.2", features = ["js"] } +getrandom = { version = "0.2", features = ["js"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tokio = { version = "^1", features = ["macros", "test-util"] } +tokio = { version = "1.0", features = ["macros", "test-util"] } [dev-dependencies] did-key = "0.1" -serde_ipld_dagcbor = "~0.2" -wasm-bindgen-test = "~0.3" +serde_ipld_dagcbor = "0.2" +wasm-bindgen-test = "0.3" diff --git a/ucan/src/chain.rs b/ucan/src/chain.rs index c26e63d5..250221a2 100644 --- a/ucan/src/chain.rs +++ b/ucan/src/chain.rs @@ -8,9 +8,13 @@ use crate::{ ucan::Ucan, }; use anyhow::{anyhow, Result}; -use async_recursion::async_recursion; use cid::Cid; -use std::{collections::BTreeSet, fmt::Debug}; +use std::{ + collections::BTreeSet, + convert::TryFrom, + fmt::{self, Debug}, + str::FromStr, +}; const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSemantics {}; @@ -38,130 +42,129 @@ where } /// A deserialized chain of ancestral proofs that are linked to a UCAN -#[derive(Debug)] -pub struct ProofChain { +pub struct ProofChain +where + S: UcanJwtStore, +{ ucan: Ucan, - proofs: Vec, + proofs: Vec>, redelegations: BTreeSet, + did_parser: Option, + store: S, } -impl ProofChain { - /// Instantiate a [ProofChain] from a [Ucan], given a [UcanJwtStore] and [DidParser] - #[cfg_attr(target_arch = "wasm32", async_recursion(?Send))] - #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] - pub async fn from_ucan( - ucan: Ucan, - did_parser: &mut DidParser, - store: &S, - ) -> Result - where - S: UcanJwtStore, - { - ucan.validate(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?; - proof_chain.validate_link_to(&ucan)?; - proofs.push(proof_chain); - } - - let mut redelegations = BTreeSet::::new(); - - for capability in CapabilityIterator::new(&ucan, &PROOF_DELEGATION_SEMANTICS) { - match capability.with() { - With::Resource { - kind: Resource::Scoped(ProofSelection::All), - } => { - for index in 0..proofs.len() { - redelegations.insert(index); - } - } - With::Resource { - kind: Resource::Scoped(ProofSelection::Index(index)), - } => { - if *index < proofs.len() { - redelegations.insert(*index); - } else { - return Err(anyhow!( - "Unable to redelegate proof; no proof at zero-based index {}", - index - )); - } - } - _ => continue, - } - } +impl Debug for ProofChain +where + S: UcanJwtStore, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Point") + .field("ucan", &self.ucan) + .field("proofs", &self.proofs) + .field("redelegations", &self.redelegations) + .finish() + } +} - Ok(ProofChain { +impl ProofChain +where + S: UcanJwtStore + Default, +{ + pub fn from_ucan(ucan: Ucan) -> Self { + Self { ucan, - proofs, - redelegations, - }) + proofs: vec![], + redelegations: BTreeSet::::new(), + did_parser: None, + store: S::default(), + } } - /// 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 - where - S: UcanJwtStore, - { - Self::try_from_token_string(&store.require_token(cid).await?, did_parser, store).await + pub async fn with_parser(mut self, mut did_parser: DidParser) -> Result> { + self.ucan.validate(&mut did_parser).await?; + self.did_parser = Some(did_parser); + Ok(self) } - /// 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, - did_parser: &mut DidParser, - store: &S, - ) -> Result - where - S: UcanJwtStore, - { - let ucan = Ucan::try_from(ucan_token_string)?; - Self::from_ucan(ucan, did_parser, store).await + pub fn with_store(mut self, store: S) -> Self { + self.store = store; + self } - fn validate_link_to(&self, ucan: &Ucan) -> Result<()> { - let audience = self.ucan.audience(); - let issuer = ucan.issuer(); - - match audience == issuer { - true => match self.ucan.lifetime_encompasses(ucan) { - true => Ok(()), - false => Err(anyhow!("Invalid UCAN link: lifetime exceeds attenuation")), - }, - false => Err(anyhow!( - "Invalid UCAN link: audience {} does not match issuer {}", - audience, - issuer - )), - } + pub async fn from_cid(cid: &Cid, store: S) -> Result { + let ucan_token = store.require_token(cid).await?; + Ok(Self { + ucan: Ucan::try_from(ucan_token)?, + proofs: vec![], + redelegations: BTreeSet::::new(), + did_parser: None, + store, + }) } pub fn ucan(&self) -> &Ucan { &self.ucan } - pub fn proofs(&self) -> &Vec { + pub fn proofs(&self) -> &Vec { &self.proofs } - pub fn reduce_capabilities( + pub async fn build(mut self) -> Result> { + for cid_string in self.ucan.proofs().iter() { + let cid = Cid::try_from(cid_string.as_str())?; + let ucan_token = self.store.require_token(&cid).await?; + let proof_chain = ProofChain::try_from(ucan_token)?; + proof_chain.validate_link_to(&self.ucan)?; + self.proofs.push(proof_chain); + + for capability in CapabilityIterator::new(&self.ucan, &PROOF_DELEGATION_SEMANTICS) { + match capability.with() { + With::Resource { + kind: Resource::Scoped(ProofSelection::All), + } => { + for index in 0..self.proofs.len() { + self.redelegations.insert(index); + } + } + With::Resource { + kind: Resource::Scoped(ProofSelection::Index(index)), + } => { + if *index < self.proofs.len() { + self.redelegations.insert(*index); + } else { + return Err(anyhow!( + "Unable to redelegate proof; no proof at zero-based index {}", + index + )); + } + } + _ => continue, + } + } + } + + Ok(ProofChain { + ucan: self.ucan, + proofs: self.proofs, + redelegations: self.redelegations, + did_parser: self.did_parser, + store: self.store, + }) + } + + pub fn reduce_capabilities( &self, semantics: &Semantics, - ) -> Vec> + ) -> Vec> where - Semantics: CapabilitySemantics, - S: Scope, + Semantics: CapabilitySemantics, + Sc: Scope, A: Action, { // Get the set of inherited attenuations (excluding redelegations) // before further attenuating by own lifetime and capabilities: - let ancestral_capability_infos: Vec> = self + let ancestral_capability_infos: Vec> = self .proofs .iter() .enumerate() @@ -176,7 +179,7 @@ impl ProofChain { // Get the set of capabilities that are blanket redelegated from // ancestor proofs (via the prf: resource): - let mut redelegated_capability_infos: Vec> = self + let mut redelegated_capability_infos: Vec> = self .redelegations .iter() .flat_map(|index| { @@ -199,16 +202,17 @@ impl ProofChain { // Get the claimed attenuations of this ucan, cross-checking ancestral // attenuations to discover the originating authority - let mut self_capability_infos: Vec> = match self.proofs.len() { - 0 => self_capabilities_iter + let mut self_capability_infos: Vec> = if self.proofs.is_empty() { + self_capabilities_iter .map(|capability| CapabilityInfo { originators: BTreeSet::from_iter(vec![self.ucan.issuer().to_string()]), capability, not_before: *self.ucan.not_before(), expires_at: *self.ucan.expires_at(), }) - .collect(), - _ => self_capabilities_iter + .collect() + } else { + self_capabilities_iter .map(|capability| { let mut originators = BTreeSet::::new(); @@ -235,12 +239,12 @@ impl ProofChain { expires_at: *self.ucan.expires_at(), } }) - .collect(), + .collect() }; self_capability_infos.append(&mut redelegated_capability_infos); - let mut merged_capability_infos = Vec::>::new(); + let mut merged_capability_infos = Vec::>::new(); // Merge redundant capabilities (accounting for redelegation), ensuring // that discrete originators are aggregated as we go @@ -262,4 +266,56 @@ impl ProofChain { merged_capability_infos } + + fn validate_link_to(&self, ucan: &Ucan) -> Result<()> { + let audience = self.ucan.audience(); + let issuer = ucan.issuer(); + + match audience == issuer { + true if self.ucan.lifetime_encompasses(ucan) => Ok(()), + true => Err(anyhow!("Invalid UCAN link: lifetime exceeds attenuation")), + false => Err(anyhow!( + "Invalid UCAN link: audience {} does not match issuer {}", + audience, + issuer + )), + } + } +} + +/// Deserialize an encoded UCAN token string reference into a default ProofChain. +impl<'a, S> TryFrom<&'a str> for ProofChain +where + S: UcanJwtStore + Default, +{ + type Error = anyhow::Error; + + fn try_from(ucan_token: &str) -> Result { + ProofChain::from_str(ucan_token) + } +} + +/// Deserialize an encoded UCAN token string into a default ProofChain. +impl TryFrom for ProofChain +where + S: UcanJwtStore + Default, +{ + type Error = anyhow::Error; + + fn try_from(ucan_token: String) -> Result { + ProofChain::from_str(ucan_token.as_str()) + } +} + +/// Deserialize an encoded UCAN token string reference into a default ProofChain. +impl FromStr for ProofChain +where + S: UcanJwtStore + Default, +{ + type Err = anyhow::Error; + + fn from_str(ucan_token: &str) -> Result { + let ucan = Ucan::try_from(ucan_token)?; + Ok(Self::from_ucan(ucan)) + } } diff --git a/ucan/src/ipld/ucan.rs b/ucan/src/ipld/ucan.rs index 4477dde4..050b27f8 100644 --- a/ucan/src/ipld/ucan.rs +++ b/ucan/src/ipld/ucan.rs @@ -8,7 +8,7 @@ use crate::{ use cid::Cid; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{convert::TryFrom, str::FromStr}; +use std::{borrow::Borrow, convert::TryFrom, str::FromStr}; #[derive(Serialize, Deserialize)] pub struct UcanIpld { @@ -27,6 +27,14 @@ pub struct UcanIpld { pub nbf: Option, } +impl TryFrom for UcanIpld { + type Error = anyhow::Error; + + fn try_from(ucan: Ucan) -> Result { + ucan.borrow().try_into() + } +} + impl TryFrom<&Ucan> for UcanIpld { type Error = anyhow::Error; diff --git a/ucan/src/lib.rs b/ucan/src/lib.rs index 7cbd8998..03c1c5fe 100644 --- a/ucan/src/lib.rs +++ b/ucan/src/lib.rs @@ -52,16 +52,21 @@ //! // You must bring your own key support //! ]; //! -//! async fn get_capabilities<'a, Semantics, S, A, Store>(ucan_token: &'a str, semantics: &'a Semantics, store: &'a Store) -> Result>, anyhow::Error> +//! async fn get_capabilities<'a, Semantics, S, A, Store>(ucan_token: &'a str, semantics: &'a Semantics, store: Store) -> Result>, anyhow::Error> //! where //! Semantics: CapabilitySemantics, //! S: Scope, //! A: Action, -//! Store: UcanJwtStore +//! Store: UcanJwtStore + Default //! { //! 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(ucan_token)? +//! .with_parser(did_parser) +//! .await? +//! .with_store(store) +//! .build() +//! .await? //! .reduce_capabilities(semantics)) //! } //! ``` diff --git a/ucan/src/tests/attenuation.rs b/ucan/src/tests/attenuation.rs index a4d58852..6cef22f2 100644 --- a/ucan/src/tests/attenuation.rs +++ b/ucan/src/tests/attenuation.rs @@ -18,7 +18,7 @@ wasm_bindgen_test_configure!(run_in_browser); #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_works_with_a_simple_example() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let did_parser = DidParser::new(SUPPORTED_KEYS); let email_semantics = EmailSemantics {}; let send_email_as_alice = email_semantics @@ -56,10 +56,15 @@ pub async fn it_works_with_a_simple_example() { .await .unwrap(); - let chain = - ProofChain::try_from_token_string(attenuated_token.as_str(), &mut did_parser, &store) - .await - .unwrap(); + let chain = ProofChain::try_from(attenuated_token.as_str()) + .unwrap() + .with_parser(did_parser) + .await + .unwrap() + .with_store(store) + .build() + .await + .unwrap(); let capability_infos = chain.reduce_capabilities(&email_semantics); @@ -78,7 +83,7 @@ pub async fn it_works_with_a_simple_example() { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let did_parser = DidParser::new(SUPPORTED_KEYS); let email_semantics = EmailSemantics {}; let send_email_as_bob = email_semantics @@ -115,7 +120,13 @@ 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) + let capability_infos = ProofChain::try_from(ucan_token) + .unwrap() + .with_parser(did_parser) + .await + .unwrap() + .with_store(store) + .build() .await .unwrap() .reduce_capabilities(&email_semantics); @@ -135,7 +146,7 @@ pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_finds_the_right_proof_chain_for_the_originator() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let did_parser = DidParser::new(SUPPORTED_KEYS); let email_semantics = EmailSemantics {}; let send_email_as_bob = email_semantics @@ -193,9 +204,16 @@ 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(ucan_token.as_str()) + .unwrap() + .with_parser(did_parser) + .await + .unwrap() + .with_store(store) + .build() .await .unwrap(); + let capability_infos = proof_chain.reduce_capabilities(&email_semantics); assert_eq!(capability_infos.len(), 2); @@ -228,7 +246,7 @@ pub async fn it_finds_the_right_proof_chain_for_the_originator() { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_reports_all_chain_options() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let did_parser = DidParser::new(SUPPORTED_KEYS); let email_semantics = EmailSemantics {}; let send_email_as_alice = email_semantics @@ -282,9 +300,16 @@ 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(ucan_token) + .unwrap() + .with_parser(did_parser) + .await + .unwrap() + .with_store(store) + .build() .await .unwrap(); + let capability_infos = proof_chain.reduce_capabilities(&email_semantics); assert_eq!(capability_infos.len(), 1); diff --git a/ucan/src/tests/chain.rs b/ucan/src/tests/chain.rs index ddb616d6..e38c3601 100644 --- a/ucan/src/tests/chain.rs +++ b/ucan/src/tests/chain.rs @@ -16,7 +16,7 @@ wasm_bindgen_test_configure!(run_in_browser); #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_decodes_deep_ucan_chains() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let did_parser = DidParser::new(SUPPORTED_KEYS); let leaf_ucan = UcanBuilder::default() .issued_by(&identities.alice_key) @@ -47,10 +47,15 @@ pub async fn it_decodes_deep_ucan_chains() { .await .unwrap(); - let chain = - ProofChain::try_from_token_string(delegated_token.as_str(), &mut did_parser, &store) - .await - .unwrap(); + let chain = ProofChain::try_from(delegated_token) + .unwrap() + .with_parser(did_parser) + .await + .unwrap() + .with_store(store) + .build() + .await + .unwrap(); assert_eq!(chain.ucan().audience(), &identities.mallory_did); assert_eq!( @@ -63,7 +68,7 @@ pub async fn it_decodes_deep_ucan_chains() { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_fails_with_incorrect_chaining() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let did_parser = DidParser::new(SUPPORTED_KEYS); let leaf_ucan = UcanBuilder::default() .issued_by(&identities.alice_key) @@ -94,8 +99,14 @@ pub async fn it_fails_with_incorrect_chaining() { .await .unwrap(); - let parse_token_result = - ProofChain::try_from_token_string(delegated_token.as_str(), &mut did_parser, &store).await; + let parse_token_result = ProofChain::try_from(delegated_token) + .unwrap() + .with_parser(did_parser) + .await + .unwrap() + .with_store(store) + .build() + .await; assert!(parse_token_result.is_err()); } @@ -104,7 +115,6 @@ pub async fn it_fails_with_incorrect_chaining() { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_can_be_instantiated_by_cid() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); let leaf_ucan = UcanBuilder::default() .issued_by(&identities.alice_key) @@ -138,7 +148,10 @@ 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, store) + .await + .unwrap() + .build() .await .unwrap(); @@ -153,7 +166,7 @@ pub async fn it_can_be_instantiated_by_cid() { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] pub async fn it_can_handle_multiple_leaves() { let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let did_parser = DidParser::new(SUPPORTED_KEYS); let leaf_ucan_1 = UcanBuilder::default() .issued_by(&identities.alice_key) @@ -199,7 +212,13 @@ pub async fn it_can_handle_multiple_leaves() { .await .unwrap(); - ProofChain::try_from_token_string(&delegated_token, &mut did_parser, &store) + ProofChain::try_from(delegated_token) + .unwrap() + .with_parser(did_parser) + .await + .unwrap() + .with_store(store) + .build() .await .unwrap(); } diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs index 7bbb0aae..9f991e19 100644 --- a/ucan/src/ucan.rs +++ b/ucan/src/ucan.rs @@ -194,7 +194,7 @@ impl TryFrom for Cid { } } -/// Deserialize an encoded UCAN token string reference into a UCAN +/// Deserialize an encoded UCAN token string reference into a UCAN. impl<'a> TryFrom<&'a str> for Ucan { type Error = anyhow::Error; @@ -203,7 +203,7 @@ impl<'a> TryFrom<&'a str> for Ucan { } } -/// Deserialize an encoded UCAN token string into a UCAN +/// Deserialize an encoded UCAN token string into a UCAN. impl TryFrom for Ucan { type Error = anyhow::Error; @@ -212,7 +212,7 @@ impl TryFrom for Ucan { } } -/// Deserialize an encoded UCAN token string reference into a UCAN +/// Deserialize an encoded UCAN token string reference into a UCAN. impl FromStr for Ucan { type Err = anyhow::Error;