diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14f2c3fb..182d1463 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: fmt name: fmt description: Format rust files. - entry: cargo +nightly fmt + entry: cargo fmt language: system types: [rust] args: ["--all", "--", "--check"] diff --git a/.rustfmt.toml b/.rustfmt.toml index 56de2ddf..b67f787b 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,2 +1,2 @@ edition = "2021" -imports_granularity = "Crate" +#imports_granularity = "Crate" diff --git a/ucan-key-support/Cargo.toml b/ucan-key-support/Cargo.toml index e8cbe332..77fd9ca0 100644 --- a/ucan-key-support/Cargo.toml +++ b/ucan-key-support/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/cdata/rs-ucan/" homepage = "https://github.com/cdata/rs-ucan" license = "Apache-2.0" readme = "README.md" -version = "0.1.7" +version = "0.1.8" [features] default = [] @@ -25,10 +25,10 @@ async-trait = "0.1" bs58 = "0.5" ed25519-zebra = "3.1" log = "0.4" -rsa = "0.9" p256 = "0.13" +rsa = "0.9" sha2 = { version = "0.10", features = ["oid"] } -ucan = { path = "../ucan", version = "0.4.0" } +ucan = { path = "../ucan", version = ">=0.4.0" } [build-dependencies] npm_rs = "1.0" diff --git a/ucan-key-support/src/ed25519.rs b/ucan-key-support/src/ed25519.rs index 4de05858..9fea8fad 100644 --- a/ucan-key-support/src/ed25519.rs +++ b/ucan-key-support/src/ed25519.rs @@ -14,6 +14,12 @@ pub fn bytes_to_ed25519_key(bytes: Vec) -> Result> { Ok(Box::new(Ed25519KeyMaterial(public_key, None))) } +pub fn bytes_to_ed25519_private_key(bytes: Vec) -> Result> { + let private_key = Ed25519PrivateKey::try_from(bytes.as_slice())?; + let public_key = Ed25519PublicKey::from(&private_key); + Ok(Box::new(Ed25519KeyMaterial(public_key, Some(private_key)))) +} + #[derive(Clone)] pub struct Ed25519KeyMaterial(pub Ed25519PublicKey, pub Option); diff --git a/ucan-key-support/src/p256.rs b/ucan-key-support/src/p256.rs index 10d2905f..64b1eb7c 100644 --- a/ucan-key-support/src/p256.rs +++ b/ucan-key-support/src/p256.rs @@ -16,6 +16,12 @@ pub fn bytes_to_p256_key(bytes: Vec) -> Result> { Ok(Box::new(P256KeyMaterial(public_key, None))) } +pub fn bytes_to_p256_private_key(bytes: Vec) -> Result> { + let private_key = P256PrivateKey::try_from(bytes.as_slice())?; + let public_key = P256PublicKey::from(&private_key); + Ok(Box::new(P256KeyMaterial(public_key, Some(private_key)))) +} + /// Support for NIST P-256 keys, aka secp256r1, aka ES256 #[derive(Clone)] pub struct P256KeyMaterial(pub P256PublicKey, pub Option); diff --git a/ucan-key-support/src/rsa.rs b/ucan-key-support/src/rsa.rs index 29219115..9a6b6df7 100644 --- a/ucan-key-support/src/rsa.rs +++ b/ucan-key-support/src/rsa.rs @@ -12,7 +12,7 @@ use ucan::crypto::{JwtSignatureAlgorithm, KeyMaterial}; pub use ucan::crypto::did::RSA_MAGIC_BYTES; pub fn bytes_to_rsa_key(bytes: Vec) -> Result> { - println!("Trying to parse RSA key..."); + // println!("Trying to parse RSA key..."); // NOTE: DID bytes are PKCS1, but we store RSA keys as PKCS8 let public_key = RsaPublicKey::from_pkcs1_der(&bytes)?; diff --git a/ucan/Cargo.toml b/ucan/Cargo.toml index 79967795..01d3853a 100644 --- a/ucan/Cargo.toml +++ b/ucan/Cargo.toml @@ -13,7 +13,7 @@ repository = "https://github.com/cdata/rs-ucan/" homepage = "https://github.com/cdata/rs-ucan" license = "Apache-2.0" readme = "README.md" -version = "0.4.0" +version = "0.5.0" edition = "2021" [features] @@ -23,20 +23,21 @@ default = [] anyhow = "1.0" async-recursion = "1.0" async-trait = "0.1" -base64 = "0.21" +base64 = "0.22" bs58 = "0.5" -cid = "0.10" +cid = { version = "0.11", features = ["serde"] } futures = "0.3" instant = { version = "0.1", features = ["wasm-bindgen"] } libipld-core = { version = "0.16", features = ["serde-codec", "serde"] } libipld-json = "0.16" log = "0.4" +multihash-codetable = { version = "0.1", features = ["sha2", "blake3", "blake2b"] } rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -strum = "0.24" -strum_macros = "0.25" -unsigned-varint = "0.7" +strum = "0.26" +strum_macros = "0.26" +unsigned-varint = "0.8" url = "2.0" [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -48,5 +49,5 @@ tokio = { version = "^1", features = ["macros", "test-util"] } [dev-dependencies] did-key = "0.2" -serde_ipld_dagcbor = "0.3" +serde_ipld_dagcbor = "0.6" wasm-bindgen-test = "0.3" diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs index 44fb9cb9..75721e8f 100644 --- a/ucan/src/builder.rs +++ b/ucan/src/builder.rs @@ -9,8 +9,8 @@ use crate::{ }; use anyhow::{anyhow, Result}; use base64::Engine; -use cid::multihash::Code; use log::warn; +use multihash_codetable::Code; use rand::Rng; use serde::{de::DeserializeOwned, Serialize}; @@ -122,6 +122,8 @@ where facts: FactsMap, proofs: Vec, add_nonce: bool, + + add_proof_facts: bool, } impl<'a, K> Default for UcanBuilder<'a, K> @@ -150,6 +152,8 @@ where facts: BTreeMap::new(), proofs: Vec::new(), add_nonce: false, + + add_proof_facts: false, } } } @@ -206,24 +210,75 @@ where self } + /// Add facts or proofs of knowledge to this UCAN. + pub fn with_facts(mut self, facts: &[(String, T)]) -> Self { + let f: Vec<(String, serde_json::Value)> = facts + .iter() + .map(|k| { + ( + k.0.to_owned(), + serde_json::to_value(&k.1).unwrap_or(serde_json::json!("null")), + ) + }) + .collect(); + self.facts.extend(f); + self + } + /// Will ensure that the built UCAN includes a number used once. pub fn with_nonce(mut self) -> Self { self.add_nonce = true; self } + /// Will add a collection of proof tokens (if any) to the facts field "prf". + pub fn with_add_proof_facts(mut self, add_proof_facts: bool) -> Self { + self.add_proof_facts = add_proof_facts; + self + } + /// 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! /// 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 { + pub fn witnessed_by(mut self, authority: &Ucan, hasher: Option) -> Result { 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), + Ok(proof) => { + self.insert_proof(&proof, authority)?; + Ok(self) + } + Err(error) => Err(anyhow!("Failed to add authority to proofs: {}", error)), } + } - self + fn insert_proof(&mut self, proof: &cid::Cid, authority: &Ucan) -> Result<()> { + self.proofs.push(proof.to_string()); + if self.add_proof_facts { + if !self.facts.contains_key("prf") { + self.facts.insert("prf".to_owned(), serde_json::json!({})); + } + if let Some(prf_map) = self.facts.get_mut("prf") { + if let Some(prf_map) = prf_map.as_object_mut() { + prf_map.insert( + proof.to_string(), + serde_json::Value::String(authority.encode()?), + ); + } + } + } + Ok(()) + } + + // Includes a collection of UCANs in the list of proofs for the UCAN to be built. + // (see witnessed_by) + pub fn with_proofs(self, proofs: &Vec, hasher: Option) -> Result { + let mut s = self; + for authority in proofs { + s = s.witnessed_by(authority, hasher)?; + } + + Ok(s) } /// Claim a capability by inheritance (from an authorizing proof) or @@ -254,26 +309,26 @@ where /// 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 { + pub fn delegating_from(mut self, authority: &Ucan, hasher: Option) -> Result { 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; + self.insert_proof(&proof, authority)?; let proof_delegation = ProofDelegationSemantics {}; - let capability = - proof_delegation.parse(&format!("prf:{proof_index}"), "ucan/DELEGATE", None); + let capability = proof_delegation.parse(&format!("ucan:{proof}"), "ucan/*", None); match capability { Some(capability) => { self.capabilities.push(Capability::from(&capability)); } - None => warn!("Could not produce delegation capability"), + None => { + return Err(anyhow!("Could not produce delegation capability")); + } } } - Err(error) => warn!("Could not encode authoritative UCAN: {:?}", error), + Err(error) => return Err(anyhow!("Could not encode authoritative UCAN: {:?}", error)), }; - self + Ok(self) } /// Returns the default hasher ([Code::Blake3_256]) used for [Cid] encodings. diff --git a/ucan/src/capability/data.rs b/ucan/src/capability/data.rs index 307fe718..9c9c6300 100644 --- a/ucan/src/capability/data.rs +++ b/ucan/src/capability/data.rs @@ -1,5 +1,9 @@ use anyhow::anyhow; -use serde::{Deserialize, Serialize}; +use serde::{ + de::Deserializer, + ser::{SerializeMap, Serializer}, + Deserialize, Serialize, +}; use serde_json::Value; use std::{ collections::{btree_map::Iter as BTreeMapIter, BTreeMap}, @@ -63,7 +67,7 @@ type CapabilitiesIterator<'a> = FlatMap< fn((&'a String, &'a AbilitiesImpl)) -> AbilitiesMap<'a>, >; -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq)] /// The [Capabilities] struct contains capability data as a map-of-maps, matching the /// [spec](https://github.com/ucan-wg/spec#326-capabilities--attenuation). /// See `iter()` to deconstruct this map into a sequence of [Capability] datas. @@ -85,6 +89,30 @@ type CapabilitiesIterator<'a> = FlatMap< /// ``` pub struct Capabilities(CapabilitiesImpl); +impl Serialize for Capabilities { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.len()))?; + for (k, v) in self.0.iter() { + map.serialize_entry(k, v)?; + } + map.end() + } +} + +impl<'de> Deserialize<'de> for Capabilities { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let t: BTreeMap>> = + Deserialize::deserialize(deserializer)?; + Ok(Capabilities(t)) + } +} + impl Capabilities { /// Using a [FlatMap] implementation, iterate over a [Capabilities] map-of-map /// as a sequence of [Capability] datas. diff --git a/ucan/src/capability/proof.rs b/ucan/src/capability/proof.rs index 986bb578..309cdb07 100644 --- a/ucan/src/capability/proof.rs +++ b/ucan/src/capability/proof.rs @@ -1,5 +1,7 @@ use super::{Ability, CapabilitySemantics, Scope}; use anyhow::{anyhow, Result}; +use cid::Cid; +use std::fmt::Display; use url::Url; #[derive(Ord, Eq, PartialEq, PartialOrd, Clone)] @@ -14,7 +16,7 @@ impl TryFrom for ProofAction { fn try_from(value: String) -> Result { match value.as_str() { - "ucan/DELEGATE" => Ok(ProofAction::Delegate), + "ucan/*" => Ok(ProofAction::Delegate), unsupported => Err(anyhow!( "Unsupported action for proof resource ({})", unsupported @@ -23,23 +25,30 @@ impl TryFrom for ProofAction { } } -impl ToString for ProofAction { - fn to_string(&self) -> String { - match self { - ProofAction::Delegate => "ucan/DELEGATE".into(), - } +impl Display for ProofAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let action_content = match self { + ProofAction::Delegate => "ucan/*", + }; + + write!(f, "{action_content}") } } -#[derive(Eq, PartialEq, Clone)] +#[derive(Eq, PartialEq, Clone, Debug)] pub enum ProofSelection { - Index(usize), + Cid(Cid), + TheseProofs, + Did(String), + DidScheme(String, String), All, } impl Scope for ProofSelection { fn contains(&self, other: &Self) -> bool { - self == other || *self == ProofSelection::All + self == other + || *self == ProofSelection::All + || (*self == ProofSelection::TheseProofs && *other != ProofSelection::All) } } @@ -48,7 +57,7 @@ impl TryFrom for ProofSelection { fn try_from(value: Url) -> Result { match value.scheme() { - "prf" => String::from(value.path()).try_into(), + "ucan" => value.to_string().try_into(), _ => Err(anyhow!("Unrecognized URI scheme")), } } @@ -59,18 +68,40 @@ impl TryFrom for ProofSelection { fn try_from(value: String) -> Result { match value.as_str() { - "*" => Ok(ProofSelection::All), - selection => Ok(ProofSelection::Index(selection.parse::()?)), + "ucan:*" => Ok(ProofSelection::All), + "ucan:./*" => Ok(ProofSelection::TheseProofs), + selection => { + if let Some(s) = selection.strip_prefix("ucan://") { + let s: Vec<&str> = s.split('/').collect(); + if s.len() != 2 { + return Err(anyhow!("Invalid delegation URI")); + } + if s[1] == "*" { + Ok(ProofSelection::Did(s[0].to_owned())) + } else { + Ok(ProofSelection::DidScheme(s[0].to_owned(), s[1].to_owned())) + } + } else if let Some(s) = selection.strip_prefix("ucan:") { + Ok(ProofSelection::Cid(Cid::try_from(s.to_string())?)) + } else { + Err(anyhow!("Unrecognized delegation URI")) + } + } } } } -impl ToString for ProofSelection { - fn to_string(&self) -> String { - match self { - ProofSelection::Index(usize) => format!("prf:{usize}"), - ProofSelection::All => "prf:*".to_string(), - } +impl Display for ProofSelection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let proof_content = match self { + ProofSelection::Cid(cid) => format!("ucan:{cid}"), + ProofSelection::Did(did) => "ucan://".to_string() + did + "/*", + ProofSelection::DidScheme(did, scheme) => "ucan://".to_string() + did + "/" + scheme, + ProofSelection::TheseProofs => "ucan:./*".to_string(), + ProofSelection::All => "ucan:*".to_string(), + }; + + write!(f, "{proof_content}") } } diff --git a/ucan/src/capability/semantics.rs b/ucan/src/capability/semantics.rs index 65966fb4..5efd6dc9 100644 --- a/ucan/src/capability/semantics.rs +++ b/ucan/src/capability/semantics.rs @@ -1,6 +1,8 @@ -use super::{Capability, Caveat}; +use super::{proof::ProofSelection, Capability, Caveat}; use serde_json::{json, Value}; +use std::convert::TryFrom; use std::fmt::Debug; +use std::fmt::Display; use url::Url; pub trait Scope: ToString + TryFrom + PartialEq + Clone { @@ -9,50 +11,13 @@ pub trait Scope: ToString + TryFrom + PartialEq + Clone { pub trait Ability: Ord + TryFrom + ToString + Clone {} -#[derive(Clone, Eq, PartialEq)] -pub enum ResourceUri -where - S: Scope, -{ - Scoped(S), - Unscoped, -} - -impl ResourceUri -where - S: Scope, -{ - pub fn contains(&self, other: &Self) -> bool { - match self { - ResourceUri::Unscoped => true, - ResourceUri::Scoped(scope) => match other { - ResourceUri::Scoped(other_scope) => scope.contains(other_scope), - _ => false, - }, - } - } -} - -impl ToString for ResourceUri -where - S: Scope, -{ - fn to_string(&self) -> String { - match self { - ResourceUri::Unscoped => "*".into(), - ResourceUri::Scoped(value) => value.to_string(), - } - } -} - #[derive(Clone, Eq, PartialEq)] pub enum Resource where S: Scope, { - Resource { kind: ResourceUri }, - My { kind: ResourceUri }, - As { did: String, kind: ResourceUri }, + ResourceUri(S), + Ucan(ProofSelection), } impl Resource @@ -61,43 +26,32 @@ where { pub fn contains(&self, other: &Self) -> bool { match (self, other) { - ( - Resource::Resource { kind: resource }, - Resource::Resource { - kind: other_resource, - }, - ) => resource.contains(other_resource), - ( - Resource::My { kind: resource }, - Resource::My { - kind: other_resource, - }, - ) => resource.contains(other_resource), - ( - Resource::As { - did, - kind: resource, - }, - Resource::As { - did: other_did, - kind: other_resource, - }, - ) if did == other_did => resource.contains(other_resource), + (Resource::ResourceUri(resource), Resource::ResourceUri(other_resource)) => { + resource.contains(other_resource) + } + (Resource::Ucan(resource), Resource::Ucan(other_resource)) => { + resource.contains(other_resource) + } + (Resource::Ucan(resource), Resource::ResourceUri(_other_resource)) => { + // TODO is it called at all? + matches!(resource, ProofSelection::All | ProofSelection::TheseProofs) + } _ => false, } } } -impl ToString for Resource +impl Display for Resource where S: Scope, { - fn to_string(&self) -> String { - match self { - Resource::Resource { kind } => kind.to_string(), - Resource::My { kind } => format!("my:{}", kind.to_string()), - Resource::As { did, kind } => format!("as:{did}:{}", kind.to_string()), - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let resource_content = match self { + Resource::Ucan(kind) => kind.to_string(), + Resource::ResourceUri(kind) => kind.to_string(), + }; + + write!(f, "{resource_content}") } } @@ -113,34 +67,6 @@ where A::try_from(String::from(ability)).ok() } - fn extract_did(&self, path: &str) -> Option<(String, String)> { - let mut path_parts = path.split(':'); - - match path_parts.next() { - Some("did") => (), - _ => return None, - }; - - match path_parts.next() { - Some("key") => (), - _ => return None, - }; - - let value = match path_parts.next() { - Some(value) => value, - _ => return None, - }; - - Some((format!("did:key:{value}"), path_parts.collect())) - } - - fn parse_resource(&self, resource: &Url) -> Option> { - Some(match resource.path() { - "*" => ResourceUri::Unscoped, - _ => ResourceUri::Scoped(self.parse_scope(resource)?), - }) - } - fn parse_caveat(&self, caveat: Option<&Value>) -> Value { if let Some(caveat) = caveat { caveat.to_owned() @@ -157,22 +83,12 @@ where ability: &str, caveat: Option<&Value>, ) -> Option> { - let uri = Url::parse(resource).ok()?; - - let cap_resource = match uri.scheme() { - "my" => Resource::My { - kind: self.parse_resource(&uri)?, - }, - "as" => { - let (did, resource) = self.extract_did(uri.path())?; - Resource::As { - did, - kind: self.parse_resource(&Url::parse(resource.as_str()).ok()?)?, - } - } - _ => Resource::Resource { - kind: self.parse_resource(&uri)?, - }, + // "ucan://did..." cannot be parsed by "url" crate + let cap_resource = if resource.starts_with("ucan:") { + Resource::Ucan(ProofSelection::try_from(resource.to_owned()).ok()?) + } else { + let uri = Url::parse(resource).ok()?; + Resource::ResourceUri(self.parse_scope(&uri)?) }; let cap_ability = match self.parse_action(ability) { diff --git a/ucan/src/chain.rs b/ucan/src/chain.rs index 285efbba..947ba26b 100644 --- a/ucan/src/chain.rs +++ b/ucan/src/chain.rs @@ -1,7 +1,7 @@ use crate::{ capability::{ proof::{ProofDelegationSemantics, ProofSelection}, - Ability, CapabilitySemantics, CapabilityView, Resource, ResourceUri, Scope, + Ability, CapabilitySemantics, CapabilityView, Resource, Scope, }, crypto::did::DidParser, store::UcanJwtStore, @@ -10,6 +10,7 @@ use crate::{ use anyhow::{anyhow, Result}; use async_recursion::async_recursion; use cid::Cid; +use multihash_codetable::Code; use std::{collections::BTreeSet, fmt::Debug}; const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSemantics {}; @@ -42,7 +43,7 @@ where pub struct ProofChain { ucan: Ucan, proofs: Vec, - redelegations: BTreeSet, + redelegations: BTreeSet, } impl ProofChain { @@ -65,7 +66,11 @@ impl ProofChain { if let Some(ucan_proofs) = ucan.proofs() { for cid_string in ucan_proofs.iter() { let cid = Cid::try_from(cid_string.as_str())?; - let ucan_token = store.require_token(&cid).await?; + // Try to get embedded proof, then request a storage + let ucan_token = match ucan.require_token(&cid) { + Some(token) => token, + None => store.require_token(&cid).await?, + }; let proof_chain = Self::try_from_token_string(&ucan_token, now_time, did_parser, store).await?; proof_chain.validate_link_to(&ucan)?; @@ -73,34 +78,60 @@ impl ProofChain { } } - let mut redelegations = BTreeSet::::new(); + let mut redelegations = BTreeSet::::new(); for capability in ucan .capabilities() .iter() .filter_map(|cap| PROOF_DELEGATION_SEMANTICS.parse_capability(&cap)) { - match capability.resource() { - Resource::Resource { - kind: ResourceUri::Scoped(ProofSelection::All), - } => { - for index in 0..proofs.len() { - redelegations.insert(index); + match capability.resource { + Resource::Ucan(kind) => { + match kind { + ProofSelection::All | ProofSelection::TheseProofs => { + for proof in proofs.iter() { + redelegations.insert(proof.ucan.to_cid(Self::default_hasher())?); + } + } + ProofSelection::Cid(cid) => { + if proofs.iter().any(|proof| { + if let Ok(proof_cid) = proof.ucan.to_cid(Self::default_hasher()) { + proof_cid == cid + } else { + false + } + }) { + redelegations.insert(cid); + } else { + return Err(anyhow!( + "Unable to redelegate proof; CID not found {}", + cid + )); + } + } + ProofSelection::Did(did) => { + if let Some(proof) = + proofs.iter().find(|proof| proof.ucan.issuer() == did) + { + redelegations.insert(proof.ucan.to_cid(Self::default_hasher())?); + } else { + return Err(anyhow!( + "Unable to redelegate proof; DID not found {}", + did + )); + } + } + ProofSelection::DidScheme(_did, _scheme) => { + // TODO need to filter by scheme, probably not here + return Err(anyhow!( + "Unable to redelegate proof; `ucan:///` not supported" + )); + } } } - Resource::Resource { - kind: ResourceUri::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; } - _ => continue, } } @@ -184,34 +215,48 @@ impl ProofChain { let ancestral_capability_infos: Vec> = self .proofs .iter() - .enumerate() - .flat_map(|(index, ancestor_chain)| { - if self.redelegations.contains(&index) { - Vec::new() + .flat_map(|ancestor_chain| { + if let Ok(cid) = ancestor_chain.ucan.to_cid(Self::default_hasher()) { + if self.redelegations.contains(&cid) { + Vec::new() + } else { + ancestor_chain.reduce_capabilities(semantics) + } } else { - ancestor_chain.reduce_capabilities(semantics) + // skip if error + Vec::new() } }) .collect(); // Get the set of capabilities that are blanket redelegated from - // ancestor proofs (via the prf: resource): + // ancestor proofs (via the ucan: resource): let mut redelegated_capability_infos: Vec> = self .redelegations .iter() - .flat_map(|index| { - self.proofs - .get(*index) - .unwrap() - .reduce_capabilities(semantics) - .into_iter() - .map(|mut info| { - // Redelegated capabilities should be attenuated by - // this UCAN's lifetime - info.not_before = *self.ucan.not_before(); - info.expires_at = *self.ucan.expires_at(); - info - }) + .flat_map(|redelegation_cid| { + let proof_chain = self.proofs.iter().find(|proof| { + if let Ok(cid) = proof.ucan.to_cid(Self::default_hasher()) { + &cid == redelegation_cid + } else { + false + } + }); + if let Some(proof_chain) = proof_chain { + proof_chain + .reduce_capabilities(semantics) + .into_iter() + .map(|mut info| { + // Redelegated capabilities should be attenuated by + // this UCAN's lifetime + info.not_before = *self.ucan.not_before(); + info.expires_at = *self.ucan.expires_at(); + info + }) + .collect() + } else { + Vec::new() + } }) .collect(); @@ -286,4 +331,9 @@ impl ProofChain { merged_capability_infos } + + /// Returns the default hasher ([Code::Blake3_256]) used for [Cid] encodings. + pub fn default_hasher() -> Code { + Code::Blake3_256 + } } diff --git a/ucan/src/crypto/did.rs b/ucan/src/crypto/did.rs index 3322f2c9..fe7622d6 100644 --- a/ucan/src/crypto/did.rs +++ b/ucan/src/crypto/did.rs @@ -59,7 +59,7 @@ impl DidParser { self.key_cache .get(&did) .ok_or_else(|| anyhow!("Couldn't find cached key")) - .map(|key| key.clone()) + .cloned() } None => Err(anyhow!("Unrecognized magic bytes: {:?}", magic_bytes)), } diff --git a/ucan/src/store.rs b/ucan/src/store.rs index 034a4c32..bc9be7f2 100644 --- a/ucan/src/store.rs +++ b/ucan/src/store.rs @@ -1,14 +1,12 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use cid::{ - multihash::{Code, MultihashDigest}, - Cid, -}; +use cid::Cid; use libipld_core::{ codec::{Codec, Decode, Encode}, ipld::Ipld, raw::RawCodec, }; +use multihash_codetable::{Code, MultihashDigest}; use std::{ collections::HashMap, io::Cursor, diff --git a/ucan/src/tests/attenuation.rs b/ucan/src/tests/attenuation.rs index 2ef9679f..1ba820d6 100644 --- a/ucan/src/tests/attenuation.rs +++ b/ucan/src/tests/attenuation.rs @@ -42,6 +42,7 @@ pub async fn it_works_with_a_simple_example() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan, None) + .unwrap() .claiming_capability(&send_email_as_alice) .build() .unwrap() @@ -101,6 +102,7 @@ pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan, None) + .unwrap() .claiming_capability(&send_email_as_bob) .build() .unwrap() @@ -174,7 +176,9 @@ pub async fn it_finds_the_right_proof_chain_for_the_originator() { .for_audience(identities.alice_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan_alice, None) + .unwrap() .witnessed_by(&leaf_ucan_bob, None) + .unwrap() .claiming_capability(&send_email_as_alice) .claiming_capability(&send_email_as_bob) .build() @@ -264,7 +268,9 @@ pub async fn it_reports_all_chain_options() { .for_audience(identities.alice_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan_alice, None) + .unwrap() .witnessed_by(&leaf_ucan_bob, None) + .unwrap() .claiming_capability(&send_email_as_alice) .build() .unwrap() @@ -397,7 +403,7 @@ pub async fn it_validates_caveats() -> anyhow::Result<()> { .issued_by(&identities.mallory_key) .for_audience(identities.alice_did.as_str()) .with_lifetime(50) - .witnessed_by(&proof_ucan, None) + .witnessed_by(&proof_ucan, None)? .claiming_capabilities(&delegated_capabilities) .build()? .sign() diff --git a/ucan/src/tests/builder.rs b/ucan/src/tests/builder.rs index cb1fdeaf..057ac324 100644 --- a/ucan/src/tests/builder.rs +++ b/ucan/src/tests/builder.rs @@ -11,8 +11,8 @@ use crate::{ }, time::now, }; -use cid::multihash::Code; use did_key::PatchedKeyPair; +use multihash_codetable::Code; use serde_json::json; #[cfg(target_arch = "wasm32")] @@ -141,6 +141,7 @@ async fn it_prevents_duplicate_proofs() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(30) .witnessed_by(&ucan, None) + .unwrap() .claiming_capability(&attenuated_cap_1) .claiming_capability(&attenuated_cap_2) .build() @@ -180,6 +181,7 @@ pub async fn it_can_use_custom_hasher() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan, Some(Code::Blake2b256)) + .unwrap() .build() .unwrap() .sign() diff --git a/ucan/src/tests/chain.rs b/ucan/src/tests/chain.rs index d671b590..f83f606c 100644 --- a/ucan/src/tests/chain.rs +++ b/ucan/src/tests/chain.rs @@ -34,6 +34,7 @@ pub async fn it_decodes_deep_ucan_chains() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan, None) + .unwrap() .build() .unwrap() .sign() @@ -81,6 +82,7 @@ pub async fn it_fails_with_incorrect_chaining() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan, None) + .unwrap() .build() .unwrap() .sign() @@ -123,6 +125,7 @@ pub async fn it_can_be_instantiated_by_cid() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan, None) + .unwrap() .build() .unwrap() .sign() @@ -182,7 +185,9 @@ pub async fn it_can_handle_multiple_leaves() { .for_audience(identities.alice_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan_1, None) + .unwrap() .witnessed_by(&leaf_ucan_2, None) + .unwrap() .build() .unwrap() .sign() @@ -227,6 +232,7 @@ pub async fn it_can_use_a_custom_timestamp_to_validate_a_ucan() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(50) .witnessed_by(&leaf_ucan, None) + .unwrap() .build() .unwrap() .sign() diff --git a/ucan/src/tests/fixtures/store.rs b/ucan/src/tests/fixtures/store.rs index bd158760..30f5878f 100644 --- a/ucan/src/tests/fixtures/store.rs +++ b/ucan/src/tests/fixtures/store.rs @@ -1,14 +1,12 @@ use crate::store::{UcanStore, UcanStoreConditionalSend}; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use cid::{ - multihash::{Code, MultihashDigest}, - Cid, -}; +use cid::Cid; use libipld_core::{ codec::{Codec, Decode, Encode}, raw::RawCodec, }; +use multihash_codetable::{Code, MultihashDigest}; use std::{ collections::HashMap, io::Cursor, diff --git a/ucan/src/tests/helpers.rs b/ucan/src/tests/helpers.rs index c178a4c0..03cde52b 100644 --- a/ucan/src/tests/helpers.rs +++ b/ucan/src/tests/helpers.rs @@ -48,7 +48,9 @@ pub async fn scaffold_ucan_builder(identities: &Identities) -> Result Option { + if let Some(facts) = &self.payload.fct { + if let Some(fact_prf) = facts.get("prf") { + if let Some(fact_prf) = fact_prf.as_object() { + if let Some(token) = fact_prf.get(&cid.to_string()) { + return token.as_str().map(|token| token.to_owned()); + } + } + } + } + None + } } /// Deserialize an encoded UCAN token string reference into a UCAN