From 0de909af905c46b7e3abbfecc1e486aad1815031 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 29 Jul 2024 17:49:19 +0100 Subject: [PATCH 01/27] Add meadowcap crate, CommunalCapability. --- Cargo.lock | 8 + Cargo.toml | 4 +- data-model/src/grouping/area.rs | 2 +- data-model/src/grouping/range.rs | 24 +++ meadowcap/Cargo.toml | 14 ++ meadowcap/src/lib.rs | 260 +++++++++++++++++++++++++++++++ 6 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 meadowcap/Cargo.toml create mode 100644 meadowcap/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e56fdbd..a5172ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,6 +334,14 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4958ec1997b05011d5c786bf4093cd48578bd9be2737350ab38659694083ddde" +[[package]] +name = "meadowcap" +version = "0.1.0" +dependencies = [ + "ufotofu", + "willow-data-model", +] + [[package]] name = "once_cell" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index 3d787a4..2082c5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] -members = ["data-model", "earthstar", "fuzz"] +members = ["data-model", "earthstar", "fuzz", "meadowcap"] resolver = "2" [workspace.lints.clippy] -type_complexity = "allow" \ No newline at end of file +type_complexity = "allow" diff --git a/data-model/src/grouping/area.rs b/data-model/src/grouping/area.rs index 04dc1a5..f99e3ca 100644 --- a/data-model/src/grouping/area.rs +++ b/data-model/src/grouping/area.rs @@ -63,7 +63,7 @@ where /// A grouping of entries. /// [Definition](https://willowprotocol.org/specs/grouping-entries/index.html#areas). -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct Area { /// To be included in this [`Area`], an [`Entry`]’s `subspace_id` must be equal to the subspace_id, unless it is any. subspace: AreaSubspace, diff --git a/data-model/src/grouping/range.rs b/data-model/src/grouping/range.rs index c94c2f5..ffc3872 100644 --- a/data-model/src/grouping/range.rs +++ b/data-model/src/grouping/range.rs @@ -118,6 +118,18 @@ impl PartialOrd Clone for RangeEnd +where + T: Ord + Clone, +{ + fn clone(&self) -> Self { + match self { + RangeEnd::Closed(val) => RangeEnd::Closed(val.clone()), + RangeEnd::Open => RangeEnd::Open, + } + } +} + #[cfg(feature = "dev")] impl<'a, T> Arbitrary<'a> for RangeEnd where @@ -235,6 +247,18 @@ impl PartialOrd for Range { } } +impl Clone for Range +where + T: Ord + Clone, +{ + fn clone(&self) -> Self { + Self { + start: self.start.clone(), + end: self.end.clone(), + } + } +} + #[cfg(feature = "dev")] impl<'a, T> Arbitrary<'a> for Range where diff --git a/meadowcap/Cargo.toml b/meadowcap/Cargo.toml new file mode 100644 index 0000000..1a857cf --- /dev/null +++ b/meadowcap/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "meadowcap" +version = "0.1.0" +edition = "2021" + +[dependencies] +ufotofu = "0.3.0" + +[dependencies.willow-data-model] +path = "../data-model" +features = ["dev"] + +[lints] +workspace = true diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs new file mode 100644 index 0000000..62d65b6 --- /dev/null +++ b/meadowcap/src/lib.rs @@ -0,0 +1,260 @@ +use ufotofu::{local_nb::Consumer, sync::consumer::IntoVec}; +use willow_data_model::{ + encoding::{parameters::Encodable, relativity::RelativeEncodable}, + grouping::area::Area, + parameters::{NamespaceId, SubspaceId}, +}; + +/// Can be used to sign a bytestring. +pub trait Signing { + fn corresponding_public_key(&self) -> PublicKey; + fn sign(&self, bytestring: &[u8]) -> Signature; +} + +/// Indicates that this is a verifiable signature. +pub trait Verifiable { + fn verify(&self, public_key: &PublicKey, bytestring: &[u8]) -> bool; +} + +/// A delegation of access rights to a user for a given area. +pub type Delegation< + const MCL: usize, + const MCC: usize, + const MPL: usize, + UserPublicKey, + UserSignature, +> = ( + Area, + UserPublicKey, + UserSignature, +); + +/// A mode granting read or write access to some [`Area`]. +#[derive(Clone)] +pub enum AccessMode { + Read, + Write, +} + +/// Returned when an attempt to delegate a capability failed. +pub enum FailedDelegationError< + const MCL: usize, + const MCC: usize, + const MPL: usize, + UserPublicKey: SubspaceId, +> { + /// The granted area of the capability we tried to delegate from did not include the given area. + AreaNotIncluded((Area, UserPublicKey)), + /// The given secret did not correspond to the receiver of the capability we tried to delegate from. + WrongSecretForUser(UserPublicKey), +} + +/// Returned when an existing delegation was found to be invalid. +pub enum InvalidDelegationError< + const MCL: usize, + const MCC: usize, + const MPL: usize, + UserPublicKey: SubspaceId, + UserSignature, +> { + /// The delegation's area was not included by the granted area of the capability we tried to add it to. + AreaNotIncluded((Area, UserPublicKey)), + /// The signature of the delegation was not valid for the recevier of the capability we tried to add the delegation to. + InvalidSignature((UserPublicKey, UserSignature)), +} + +/// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). +/// +/// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_capabilities). +pub struct CommunalCapability< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + UserPublicKey, + UserSignature, +> where + NamespacePublicKey: NamespaceId + Encodable, + UserPublicKey: SubspaceId + Encodable, + UserSignature: Encodable + Verifiable, +{ + access_mode: AccessMode, + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + delegations: Vec>, +} + +impl< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + UserPublicKey, + UserSignature, + > CommunalCapability +where + NamespacePublicKey: NamespaceId + Encodable, + UserPublicKey: SubspaceId + Encodable, + UserSignature: Encodable + Verifiable + Clone, +{ + /// Create a new communal capability granting access to the [`SubspaceId`] corresponding to the given [`UserPublicKey`]. + pub fn new( + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + access_mode: AccessMode, + ) -> Self { + Self { + access_mode, + namespace_key, + user_key, + delegations: Vec::new(), + } + } + + /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. + /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). + pub async fn delegate( + &self, + secret_key: UserSecretKey, + new_user: UserPublicKey, + new_area: Area, + ) -> Result> + where + UserSecretKey: Signing, + { + let prev_area = self.granted_area(); + + if !prev_area.includes_area(&new_area) { + return Err(FailedDelegationError::AreaNotIncluded((new_area, new_user))); + } + + let prev_user = self.receiver(); + + if &secret_key.corresponding_public_key() != prev_user { + return Err(FailedDelegationError::WrongSecretForUser(new_user)); + } + + let handover = self.handover(&new_area, &new_user).await; + let signature = secret_key.sign(&handover); + + let mut new_delegations = self.delegations.clone(); + + new_delegations.push((new_area, new_user, signature)); + + Ok(Self { + access_mode: self.access_mode.clone(), + namespace_key: self.namespace_key.clone(), + user_key: self.user_key.clone(), + delegations: new_delegations, + }) + } + + /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. + pub async fn append_existing_delegation( + &mut self, + delegation: Delegation, + ) -> Result<(), InvalidDelegationError> { + let (new_area, new_user, new_sig) = delegation; + + if !self.granted_area().includes_area(&new_area) { + return Err(InvalidDelegationError::AreaNotIncluded(( + new_area, new_user, + ))); + } + + let handover = self.handover(&new_area, &new_user).await; + + let prev_receiver = self.receiver(); + + let is_authentic = new_sig.verify(prev_receiver, &handover); + + if !is_authentic { + return Err(InvalidDelegationError::InvalidSignature(( + new_user, new_sig, + ))); + } + + self.delegations.push((new_area, new_user, new_sig)); + + Ok(()) + } + + /// The kind of access this capability grants. + pub fn access_mode(&self) -> &AccessMode { + &self.access_mode + } + + /// The user to whom this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver) + pub fn receiver(&self) -> &UserPublicKey { + if self.delegations.is_empty() { + return &self.user_key; + } + + // We can unwrap here because we know delegations isn't empty. + let (_, receiver, _) = self.delegations.last().unwrap(); + + receiver + } + + /// The [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) for which this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_namespace) + pub fn granted_namespace(&self) -> &NamespacePublicKey { + &self.namespace_key + } + + /// The [`Area`] for which this capability grants access. + /// + /// [Definition](`https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_namespace`) + pub fn granted_area(&self) -> Area { + if self.delegations.is_empty() { + return Area::new_subspace(self.user_key.clone()); + } + + // We can unwrap here because we know delegations isn't empty. + let (area, _, _) = self.delegations.last().unwrap(); + + area.clone() + } + + /// A bytestring to be signed for a new [`Delegation`]. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_handover) + // TODO: Make this sync. + async fn handover( + &self, + new_area: &Area, + new_user: &UserPublicKey, + ) -> Vec { + let mut consumer = IntoVec::::new(); + + if self.delegations.is_empty() { + let first_byte = match self.access_mode { + AccessMode::Read => 0x00, + AccessMode::Write => 0x01, + }; + + let prev_area = + Area::::new_subspace(self.user_key.clone()); + + // TODO: Decide whether to propagate these errors or not. + consumer.consume(first_byte).await; + self.namespace_key.encode(&mut consumer).await; + new_area.relative_encode(&prev_area, &mut consumer).await; + new_user.encode(&mut consumer).await; + + return consumer.into_vec(); + } + + // We can unwrap here because we know that self.delegations is not empty. + let (prev_area, _, prev_signature) = self.delegations.last().unwrap(); + + new_area.relative_encode(prev_area, &mut consumer).await; + prev_signature.encode(&mut consumer).await; + new_user.encode(&mut consumer).await; + + consumer.into_vec() + } +} From 10adb9bcdce246272d9e30d8b03056ffdff30e1c Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 10:38:06 +0100 Subject: [PATCH 02/27] Update meadowcap/src/lib.rs Co-authored-by: Aljoscha Meyer --- meadowcap/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 62d65b6..6983c9f 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -44,7 +44,7 @@ pub enum FailedDelegationError< UserPublicKey: SubspaceId, > { /// The granted area of the capability we tried to delegate from did not include the given area. - AreaNotIncluded((Area, UserPublicKey)), + AreaNotIncluded(Area, UserPublicKey), /// The given secret did not correspond to the receiver of the capability we tried to delegate from. WrongSecretForUser(UserPublicKey), } From e407a57afe59d4148c7aacb15d5351febe5a67d1 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 10:42:51 +0100 Subject: [PATCH 03/27] Update meadowcap/src/lib.rs Co-authored-by: Aljoscha Meyer --- meadowcap/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 6983c9f..ce7e479 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -180,6 +180,8 @@ where } /// The kind of access this capability grants. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_mode) pub fn access_mode(&self) -> &AccessMode { &self.access_mode } From e399d8f2f17cc7167027e4ef88ee5df229db53e7 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 10:53:06 +0100 Subject: [PATCH 04/27] fix that --- meadowcap/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index ce7e479..ff7c541 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -125,7 +125,7 @@ where let prev_area = self.granted_area(); if !prev_area.includes_area(&new_area) { - return Err(FailedDelegationError::AreaNotIncluded((new_area, new_user))); + return Err(FailedDelegationError::AreaNotIncluded(new_area, new_user)); } let prev_user = self.receiver(); From a80678d64b03ddf966d7b182953ac1ba174df2d6 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 11:43:16 +0100 Subject: [PATCH 05/27] Add dedicated Delegation type --- meadowcap/src/lib.rs | 73 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index ff7c541..62d95b5 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -17,17 +17,53 @@ pub trait Verifiable { } /// A delegation of access rights to a user for a given area. -pub type Delegation< +#[derive(Clone)] +pub struct Delegation< const MCL: usize, const MCC: usize, const MPL: usize, UserPublicKey, UserSignature, -> = ( - Area, - UserPublicKey, - UserSignature, -); +> where + UserPublicKey: SubspaceId, +{ + area: Area, + user: UserPublicKey, + signature: UserSignature, +} + +impl + Delegation +where + UserPublicKey: SubspaceId, +{ + pub fn new( + area: Area, + user: UserPublicKey, + signature: UserSignature, + ) -> Self { + Self { + area, + user, + signature, + } + } + + /// The granted area of this delegation. + pub fn area(&self) -> &Area { + &self.area + } + + /// The user delegated to. + pub fn user(&self) -> &UserPublicKey { + &self.user + } + + /// The signature of the user who created this delegation. + pub fn signature(&self) -> &UserSignature { + &self.signature + } +} /// A mode granting read or write access to some [`Area`]. #[derive(Clone)] @@ -139,7 +175,7 @@ where let mut new_delegations = self.delegations.clone(); - new_delegations.push((new_area, new_user, signature)); + new_delegations.push(Delegation::new(new_area, new_user, signature)); Ok(Self { access_mode: self.access_mode.clone(), @@ -154,11 +190,14 @@ where &mut self, delegation: Delegation, ) -> Result<(), InvalidDelegationError> { - let (new_area, new_user, new_sig) = delegation; + let new_area = delegation.area(); + let new_user = delegation.user(); + let new_sig = delegation.signature(); if !self.granted_area().includes_area(&new_area) { return Err(InvalidDelegationError::AreaNotIncluded(( - new_area, new_user, + new_area.clone(), + new_user.clone(), ))); } @@ -170,11 +209,12 @@ where if !is_authentic { return Err(InvalidDelegationError::InvalidSignature(( - new_user, new_sig, + new_user.clone(), + new_sig.clone(), ))); } - self.delegations.push((new_area, new_user, new_sig)); + self.delegations.push(delegation); Ok(()) } @@ -195,7 +235,8 @@ where } // We can unwrap here because we know delegations isn't empty. - let (_, receiver, _) = self.delegations.last().unwrap(); + let last_delegation = self.delegations.last().unwrap(); + let receiver = last_delegation.user(); receiver } @@ -216,9 +257,9 @@ where } // We can unwrap here because we know delegations isn't empty. - let (area, _, _) = self.delegations.last().unwrap(); + let last_delegation = self.delegations.last().unwrap(); - area.clone() + last_delegation.area().clone() } /// A bytestring to be signed for a new [`Delegation`]. @@ -251,7 +292,9 @@ where } // We can unwrap here because we know that self.delegations is not empty. - let (prev_area, _, prev_signature) = self.delegations.last().unwrap(); + let last_delegation = self.delegations.last().unwrap(); + let prev_area = last_delegation.area(); + let prev_signature = last_delegation.signature(); new_area.relative_encode(prev_area, &mut consumer).await; prev_signature.encode(&mut consumer).await; From 8b341e22ccb50693ffa1835fa9168a2fecf351ce Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 11:48:36 +0100 Subject: [PATCH 06/27] Errors with helpful field names --- meadowcap/src/lib.rs | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 62d95b5..7b52d35 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -80,7 +80,10 @@ pub enum FailedDelegationError< UserPublicKey: SubspaceId, > { /// The granted area of the capability we tried to delegate from did not include the given area. - AreaNotIncluded(Area, UserPublicKey), + AreaNotIncluded { + excluded_area: Area, + claimed_receiver: UserPublicKey, + }, /// The given secret did not correspond to the receiver of the capability we tried to delegate from. WrongSecretForUser(UserPublicKey), } @@ -94,9 +97,16 @@ pub enum InvalidDelegationError< UserSignature, > { /// The delegation's area was not included by the granted area of the capability we tried to add it to. - AreaNotIncluded((Area, UserPublicKey)), - /// The signature of the delegation was not valid for the recevier of the capability we tried to add the delegation to. - InvalidSignature((UserPublicKey, UserSignature)), + AreaNotIncluded { + excluded_area: Area, + claimed_receiver: UserPublicKey, + }, + /// The signature of the delegation was not valid for the receiver of the capability we tried to add the delegation to. + InvalidSignature { + expected_signatory: UserPublicKey, + claimed_receiver: UserPublicKey, + signature: UserSignature, + }, } /// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). @@ -161,7 +171,10 @@ where let prev_area = self.granted_area(); if !prev_area.includes_area(&new_area) { - return Err(FailedDelegationError::AreaNotIncluded(new_area, new_user)); + return Err(FailedDelegationError::AreaNotIncluded { + excluded_area: new_area, + claimed_receiver: new_user, + }); } let prev_user = self.receiver(); @@ -194,24 +207,25 @@ where let new_user = delegation.user(); let new_sig = delegation.signature(); - if !self.granted_area().includes_area(&new_area) { - return Err(InvalidDelegationError::AreaNotIncluded(( - new_area.clone(), - new_user.clone(), - ))); + if !self.granted_area().includes_area(new_area) { + return Err(InvalidDelegationError::AreaNotIncluded { + excluded_area: new_area.clone(), + claimed_receiver: new_user.clone(), + }); } - let handover = self.handover(&new_area, &new_user).await; + let handover = self.handover(new_area, new_user).await; let prev_receiver = self.receiver(); let is_authentic = new_sig.verify(prev_receiver, &handover); if !is_authentic { - return Err(InvalidDelegationError::InvalidSignature(( - new_user.clone(), - new_sig.clone(), - ))); + return Err(InvalidDelegationError::InvalidSignature { + claimed_receiver: new_user.clone(), + expected_signatory: prev_receiver.clone(), + signature: new_sig.clone(), + }); } self.delegations.push(delegation); From 11c97c113b5f95b73289af67fd500b3e88eeb663 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 15:37:15 +0100 Subject: [PATCH 07/27] Unwrap consumer results in handover --- meadowcap/src/lib.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 7b52d35..c588446 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -296,11 +296,14 @@ where let prev_area = Area::::new_subspace(self.user_key.clone()); - // TODO: Decide whether to propagate these errors or not. - consumer.consume(first_byte).await; - self.namespace_key.encode(&mut consumer).await; - new_area.relative_encode(&prev_area, &mut consumer).await; - new_user.encode(&mut consumer).await; + // We can safely unwrap all these encodings as IntoVec's error is the never type. + consumer.consume(first_byte).await.unwrap(); + self.namespace_key.encode(&mut consumer).await.unwrap(); + new_area + .relative_encode(&prev_area, &mut consumer) + .await + .unwrap(); + new_user.encode(&mut consumer).await.unwrap(); return consumer.into_vec(); } @@ -310,9 +313,13 @@ where let prev_area = last_delegation.area(); let prev_signature = last_delegation.signature(); - new_area.relative_encode(prev_area, &mut consumer).await; - prev_signature.encode(&mut consumer).await; - new_user.encode(&mut consumer).await; + // We can safely unwrap all these encodings as IntoVec's error is the never type. + new_area + .relative_encode(prev_area, &mut consumer) + .await + .unwrap(); + prev_signature.encode(&mut consumer).await.unwrap(); + new_user.encode(&mut consumer).await.unwrap(); consumer.into_vec() } From 12ed973d5aa16138553ddfed0cac42b337407757 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 16:33:50 +0100 Subject: [PATCH 08/27] Use signature crate --- Cargo.lock | 7 +++++++ meadowcap/Cargo.toml | 1 + meadowcap/src/lib.rs | 40 ++++++++++++++-------------------------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5172ed..94230c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,7 @@ checksum = "4958ec1997b05011d5c786bf4093cd48578bd9be2737350ab38659694083ddde" name = "meadowcap" version = "0.1.0" dependencies = [ + "signature", "ufotofu", "willow-data-model", ] @@ -426,6 +427,12 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + [[package]] name = "slab" version = "0.4.9" diff --git a/meadowcap/Cargo.toml b/meadowcap/Cargo.toml index 1a857cf..486e95f 100644 --- a/meadowcap/Cargo.toml +++ b/meadowcap/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +signature = "2.2.0" ufotofu = "0.3.0" [dependencies.willow-data-model] diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index c588446..63fe26d 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -1,3 +1,4 @@ +use signature::{Signer, Verifier}; use ufotofu::{local_nb::Consumer, sync::consumer::IntoVec}; use willow_data_model::{ encoding::{parameters::Encodable, relativity::RelativeEncodable}, @@ -5,17 +6,6 @@ use willow_data_model::{ parameters::{NamespaceId, SubspaceId}, }; -/// Can be used to sign a bytestring. -pub trait Signing { - fn corresponding_public_key(&self) -> PublicKey; - fn sign(&self, bytestring: &[u8]) -> Signature; -} - -/// Indicates that this is a verifiable signature. -pub trait Verifiable { - fn verify(&self, public_key: &PublicKey, bytestring: &[u8]) -> bool; -} - /// A delegation of access rights to a user for a given area. #[derive(Clone)] pub struct Delegation< @@ -121,8 +111,8 @@ pub struct CommunalCapability< UserSignature, > where NamespacePublicKey: NamespaceId + Encodable, - UserPublicKey: SubspaceId + Encodable, - UserSignature: Encodable + Verifiable, + UserPublicKey: SubspaceId + Encodable + Verifier, + UserSignature: Encodable, { access_mode: AccessMode, namespace_key: NamespacePublicKey, @@ -140,8 +130,8 @@ impl< > CommunalCapability where NamespacePublicKey: NamespaceId + Encodable, - UserPublicKey: SubspaceId + Encodable, - UserSignature: Encodable + Verifiable + Clone, + UserPublicKey: SubspaceId + Encodable + Verifier, + UserSignature: Encodable + Clone, { /// Create a new communal capability granting access to the [`SubspaceId`] corresponding to the given [`UserPublicKey`]. pub fn new( @@ -166,7 +156,7 @@ where new_area: Area, ) -> Result> where - UserSecretKey: Signing, + UserSecretKey: Signer, { let prev_area = self.granted_area(); @@ -179,13 +169,13 @@ where let prev_user = self.receiver(); - if &secret_key.corresponding_public_key() != prev_user { - return Err(FailedDelegationError::WrongSecretForUser(new_user)); - } - let handover = self.handover(&new_area, &new_user).await; let signature = secret_key.sign(&handover); + prev_user + .verify(&handover, &signature) + .map_err(|_| FailedDelegationError::WrongSecretForUser(new_user.clone()))?; + let mut new_delegations = self.delegations.clone(); new_delegations.push(Delegation::new(new_area, new_user, signature)); @@ -218,15 +208,13 @@ where let prev_receiver = self.receiver(); - let is_authentic = new_sig.verify(prev_receiver, &handover); - - if !is_authentic { - return Err(InvalidDelegationError::InvalidSignature { + prev_receiver.verify(&handover, new_sig).map_err(|_| { + InvalidDelegationError::InvalidSignature { claimed_receiver: new_user.clone(), expected_signatory: prev_receiver.clone(), signature: new_sig.clone(), - }); - } + } + })?; self.delegations.push(delegation); From d62bc06018741281dad1555087808f83e5fcff5d Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 16:50:20 +0100 Subject: [PATCH 09/27] Box<[u8]> for handover --- meadowcap/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 63fe26d..3e0231c 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -272,7 +272,7 @@ where &self, new_area: &Area, new_user: &UserPublicKey, - ) -> Vec { + ) -> Box<[u8]> { let mut consumer = IntoVec::::new(); if self.delegations.is_empty() { @@ -293,7 +293,7 @@ where .unwrap(); new_user.encode(&mut consumer).await.unwrap(); - return consumer.into_vec(); + return consumer.into_vec().into(); } // We can unwrap here because we know that self.delegations is not empty. @@ -309,6 +309,6 @@ where prev_signature.encode(&mut consumer).await.unwrap(); new_user.encode(&mut consumer).await.unwrap(); - consumer.into_vec() + consumer.into_vec().into() } } From 41f5fbac755e442ed9454cfe1e7756b6c7655f0f Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 18:06:00 +0100 Subject: [PATCH 10/27] Add OwnedCapability --- meadowcap/src/communal_capability.rs | 223 +++++++++++++++++++++++ meadowcap/src/lib.rs | 223 +---------------------- meadowcap/src/owned_capability.rs | 256 +++++++++++++++++++++++++++ 3 files changed, 482 insertions(+), 220 deletions(-) create mode 100644 meadowcap/src/communal_capability.rs create mode 100644 meadowcap/src/owned_capability.rs diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs new file mode 100644 index 0000000..a304dd4 --- /dev/null +++ b/meadowcap/src/communal_capability.rs @@ -0,0 +1,223 @@ +use signature::{Signer, Verifier}; +use ufotofu::{local_nb::Consumer, sync::consumer::IntoVec}; +use willow_data_model::{ + encoding::{parameters::Encodable, relativity::RelativeEncodable}, + grouping::area::Area, + parameters::{NamespaceId, SubspaceId}, +}; + +use crate::{AccessMode, Delegation, FailedDelegationError, InvalidDelegationError}; + +/// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). +/// +/// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_capabilities). +pub struct CommunalCapability< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + UserPublicKey, + UserSignature, +> where + NamespacePublicKey: NamespaceId + Encodable, + UserPublicKey: SubspaceId + Encodable + Verifier, + UserSignature: Encodable, +{ + access_mode: AccessMode, + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + delegations: Vec>, +} + +impl< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + UserPublicKey, + UserSignature, + > CommunalCapability +where + NamespacePublicKey: NamespaceId + Encodable, + UserPublicKey: SubspaceId + Encodable + Verifier, + UserSignature: Encodable + Clone, +{ + /// Create a new communal capability granting access to the [`SubspaceId`] corresponding to the given [`UserPublicKey`]. + pub fn new( + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + access_mode: AccessMode, + ) -> Self { + Self { + access_mode, + namespace_key, + user_key, + delegations: Vec::new(), + } + } + + /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. + /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). + pub async fn delegate( + &self, + secret_key: UserSecretKey, + new_user: UserPublicKey, + new_area: Area, + ) -> Result> + where + UserSecretKey: Signer, + { + let prev_area = self.granted_area(); + + if !prev_area.includes_area(&new_area) { + return Err(FailedDelegationError::AreaNotIncluded { + excluded_area: new_area, + claimed_receiver: new_user, + }); + } + + let prev_user = self.receiver(); + + let handover = self.handover(&new_area, &new_user).await; + let signature = secret_key.sign(&handover); + + prev_user + .verify(&handover, &signature) + .map_err(|_| FailedDelegationError::WrongSecretForUser(new_user.clone()))?; + + let mut new_delegations = self.delegations.clone(); + + new_delegations.push(Delegation::new(new_area, new_user, signature)); + + Ok(Self { + access_mode: self.access_mode.clone(), + namespace_key: self.namespace_key.clone(), + user_key: self.user_key.clone(), + delegations: new_delegations, + }) + } + + /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. + pub async fn append_existing_delegation( + &mut self, + delegation: Delegation, + ) -> Result<(), InvalidDelegationError> { + let new_area = delegation.area(); + let new_user = delegation.user(); + let new_sig = delegation.signature(); + + if !self.granted_area().includes_area(new_area) { + return Err(InvalidDelegationError::AreaNotIncluded { + excluded_area: new_area.clone(), + claimed_receiver: new_user.clone(), + }); + } + + let handover = self.handover(new_area, new_user).await; + + let prev_receiver = self.receiver(); + + prev_receiver.verify(&handover, new_sig).map_err(|_| { + InvalidDelegationError::InvalidSignature { + claimed_receiver: new_user.clone(), + expected_signatory: prev_receiver.clone(), + signature: new_sig.clone(), + } + })?; + + self.delegations.push(delegation); + + Ok(()) + } + + /// The kind of access this capability grants. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_mode) + pub fn access_mode(&self) -> &AccessMode { + &self.access_mode + } + + /// The user to whom this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver) + pub fn receiver(&self) -> &UserPublicKey { + if self.delegations.is_empty() { + return &self.user_key; + } + + // We can unwrap here because we know delegations isn't empty. + let last_delegation = self.delegations.last().unwrap(); + let receiver = last_delegation.user(); + + receiver + } + + /// The [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) for which this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_namespace) + pub fn granted_namespace(&self) -> &NamespacePublicKey { + &self.namespace_key + } + + /// The [`Area`] for which this capability grants access. + /// + /// [Definition](`https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area`) + pub fn granted_area(&self) -> Area { + if self.delegations.is_empty() { + return Area::new_subspace(self.user_key.clone()); + } + + // We can unwrap here because we know delegations isn't empty. + let last_delegation = self.delegations.last().unwrap(); + + last_delegation.area().clone() + } + + /// A bytestring to be signed for a new [`Delegation`]. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_handover) + // TODO: Make this sync. + async fn handover( + &self, + new_area: &Area, + new_user: &UserPublicKey, + ) -> Box<[u8]> { + let mut consumer = IntoVec::::new(); + + if self.delegations.is_empty() { + let first_byte = match self.access_mode { + AccessMode::Read => 0x00, + AccessMode::Write => 0x01, + }; + + let prev_area = + Area::::new_subspace(self.user_key.clone()); + + // We can safely unwrap all these encodings as IntoVec's error is the never type. + consumer.consume(first_byte).await.unwrap(); + self.namespace_key.encode(&mut consumer).await.unwrap(); + new_area + .relative_encode(&prev_area, &mut consumer) + .await + .unwrap(); + new_user.encode(&mut consumer).await.unwrap(); + + return consumer.into_vec().into(); + } + + // We can unwrap here because we know that self.delegations is not empty. + let last_delegation = self.delegations.last().unwrap(); + let prev_area = last_delegation.area(); + let prev_signature = last_delegation.signature(); + + // We can safely unwrap all these encodings as IntoVec's error is the never type. + new_area + .relative_encode(prev_area, &mut consumer) + .await + .unwrap(); + prev_signature.encode(&mut consumer).await.unwrap(); + new_user.encode(&mut consumer).await.unwrap(); + + consumer.into_vec().into() + } +} diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 3e0231c..9d2aaf9 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -1,10 +1,4 @@ -use signature::{Signer, Verifier}; -use ufotofu::{local_nb::Consumer, sync::consumer::IntoVec}; -use willow_data_model::{ - encoding::{parameters::Encodable, relativity::RelativeEncodable}, - grouping::area::Area, - parameters::{NamespaceId, SubspaceId}, -}; +use willow_data_model::{grouping::area::Area, parameters::SubspaceId}; /// A delegation of access rights to a user for a given area. #[derive(Clone)] @@ -99,216 +93,5 @@ pub enum InvalidDelegationError< }, } -/// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). -/// -/// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_capabilities). -pub struct CommunalCapability< - const MCL: usize, - const MCC: usize, - const MPL: usize, - NamespacePublicKey, - UserPublicKey, - UserSignature, -> where - NamespacePublicKey: NamespaceId + Encodable, - UserPublicKey: SubspaceId + Encodable + Verifier, - UserSignature: Encodable, -{ - access_mode: AccessMode, - namespace_key: NamespacePublicKey, - user_key: UserPublicKey, - delegations: Vec>, -} - -impl< - const MCL: usize, - const MCC: usize, - const MPL: usize, - NamespacePublicKey, - UserPublicKey, - UserSignature, - > CommunalCapability -where - NamespacePublicKey: NamespaceId + Encodable, - UserPublicKey: SubspaceId + Encodable + Verifier, - UserSignature: Encodable + Clone, -{ - /// Create a new communal capability granting access to the [`SubspaceId`] corresponding to the given [`UserPublicKey`]. - pub fn new( - namespace_key: NamespacePublicKey, - user_key: UserPublicKey, - access_mode: AccessMode, - ) -> Self { - Self { - access_mode, - namespace_key, - user_key, - delegations: Vec::new(), - } - } - - /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. - /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). - pub async fn delegate( - &self, - secret_key: UserSecretKey, - new_user: UserPublicKey, - new_area: Area, - ) -> Result> - where - UserSecretKey: Signer, - { - let prev_area = self.granted_area(); - - if !prev_area.includes_area(&new_area) { - return Err(FailedDelegationError::AreaNotIncluded { - excluded_area: new_area, - claimed_receiver: new_user, - }); - } - - let prev_user = self.receiver(); - - let handover = self.handover(&new_area, &new_user).await; - let signature = secret_key.sign(&handover); - - prev_user - .verify(&handover, &signature) - .map_err(|_| FailedDelegationError::WrongSecretForUser(new_user.clone()))?; - - let mut new_delegations = self.delegations.clone(); - - new_delegations.push(Delegation::new(new_area, new_user, signature)); - - Ok(Self { - access_mode: self.access_mode.clone(), - namespace_key: self.namespace_key.clone(), - user_key: self.user_key.clone(), - delegations: new_delegations, - }) - } - - /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. - pub async fn append_existing_delegation( - &mut self, - delegation: Delegation, - ) -> Result<(), InvalidDelegationError> { - let new_area = delegation.area(); - let new_user = delegation.user(); - let new_sig = delegation.signature(); - - if !self.granted_area().includes_area(new_area) { - return Err(InvalidDelegationError::AreaNotIncluded { - excluded_area: new_area.clone(), - claimed_receiver: new_user.clone(), - }); - } - - let handover = self.handover(new_area, new_user).await; - - let prev_receiver = self.receiver(); - - prev_receiver.verify(&handover, new_sig).map_err(|_| { - InvalidDelegationError::InvalidSignature { - claimed_receiver: new_user.clone(), - expected_signatory: prev_receiver.clone(), - signature: new_sig.clone(), - } - })?; - - self.delegations.push(delegation); - - Ok(()) - } - - /// The kind of access this capability grants. - /// - /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_mode) - pub fn access_mode(&self) -> &AccessMode { - &self.access_mode - } - - /// The user to whom this capability grants access. - /// - /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver) - pub fn receiver(&self) -> &UserPublicKey { - if self.delegations.is_empty() { - return &self.user_key; - } - - // We can unwrap here because we know delegations isn't empty. - let last_delegation = self.delegations.last().unwrap(); - let receiver = last_delegation.user(); - - receiver - } - - /// The [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) for which this capability grants access. - /// - /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_namespace) - pub fn granted_namespace(&self) -> &NamespacePublicKey { - &self.namespace_key - } - - /// The [`Area`] for which this capability grants access. - /// - /// [Definition](`https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_namespace`) - pub fn granted_area(&self) -> Area { - if self.delegations.is_empty() { - return Area::new_subspace(self.user_key.clone()); - } - - // We can unwrap here because we know delegations isn't empty. - let last_delegation = self.delegations.last().unwrap(); - - last_delegation.area().clone() - } - - /// A bytestring to be signed for a new [`Delegation`]. - /// - /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_handover) - // TODO: Make this sync. - async fn handover( - &self, - new_area: &Area, - new_user: &UserPublicKey, - ) -> Box<[u8]> { - let mut consumer = IntoVec::::new(); - - if self.delegations.is_empty() { - let first_byte = match self.access_mode { - AccessMode::Read => 0x00, - AccessMode::Write => 0x01, - }; - - let prev_area = - Area::::new_subspace(self.user_key.clone()); - - // We can safely unwrap all these encodings as IntoVec's error is the never type. - consumer.consume(first_byte).await.unwrap(); - self.namespace_key.encode(&mut consumer).await.unwrap(); - new_area - .relative_encode(&prev_area, &mut consumer) - .await - .unwrap(); - new_user.encode(&mut consumer).await.unwrap(); - - return consumer.into_vec().into(); - } - - // We can unwrap here because we know that self.delegations is not empty. - let last_delegation = self.delegations.last().unwrap(); - let prev_area = last_delegation.area(); - let prev_signature = last_delegation.signature(); - - // We can safely unwrap all these encodings as IntoVec's error is the never type. - new_area - .relative_encode(prev_area, &mut consumer) - .await - .unwrap(); - prev_signature.encode(&mut consumer).await.unwrap(); - new_user.encode(&mut consumer).await.unwrap(); - - consumer.into_vec().into() - } -} +pub mod communal_capability; +pub mod owned_capability; diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs new file mode 100644 index 0000000..4626dbd --- /dev/null +++ b/meadowcap/src/owned_capability.rs @@ -0,0 +1,256 @@ +use signature::{Error as SignatureError, Signer, Verifier}; +use ufotofu::{local_nb::Consumer, sync::consumer::IntoVec}; +use willow_data_model::{ + encoding::{parameters::Encodable, relativity::RelativeEncodable}, + grouping::area::Area, + parameters::{NamespaceId, SubspaceId}, +}; + +use crate::{AccessMode, Delegation, FailedDelegationError, InvalidDelegationError}; + +/// A capability that implements [owned namespaces](https://willowprotocol.org/specs/meadowcap/index.html#owned_namespace). +/// +/// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_capabilities). +pub struct OwnedCapability< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, +> where + NamespacePublicKey: NamespaceId + Encodable + Verifier, + UserPublicKey: SubspaceId + Encodable + Verifier, + NamespaceSignature: Encodable, + UserSignature: Encodable, +{ + access_mode: AccessMode, + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + initial_authorisation: NamespaceSignature, + delegations: Vec>, +} + +impl< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > + OwnedCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > +where + NamespacePublicKey: NamespaceId + Encodable + Verifier, + UserPublicKey: SubspaceId + Encodable + Verifier, + NamespaceSignature: Encodable + Clone, + UserSignature: Encodable + Clone, +{ + /// Create a new owned capability granting access to the [full area](https://willowprotocol.org/specs/grouping-entries/index.html#full_area) of the [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) to the given [`UserPublicKey`]. + pub async fn new( + namespace_key: NamespacePublicKey, + namespace_secret: NamespaceSecret, + user_key: UserPublicKey, + access_mode: AccessMode, + ) -> Result + where + NamespaceSecret: Signer, + { + let mut consumer = IntoVec::::new(); + + let access_byte = match access_mode { + AccessMode::Read => 0x02, + AccessMode::Write => 0x03, + }; + + consumer.consume(access_byte).await.unwrap(); + user_key.encode(&mut consumer).await.unwrap(); + + let message = consumer.into_vec(); + + let initial_authorisation = namespace_secret.sign(&message); + + namespace_key.verify(&message, &initial_authorisation)?; + + Ok(Self { + access_mode, + namespace_key, + initial_authorisation, + user_key, + delegations: Vec::new(), + }) + } + + /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. + /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). + pub async fn delegate( + &self, + secret_key: UserSecretKey, + new_user: UserPublicKey, + new_area: Area, + ) -> Result> + where + UserSecretKey: Signer, + { + let prev_area = self.granted_area(); + + if !prev_area.includes_area(&new_area) { + return Err(FailedDelegationError::AreaNotIncluded { + excluded_area: new_area, + claimed_receiver: new_user, + }); + } + + let prev_user = self.receiver(); + + let handover = self.handover(&new_area, &new_user).await; + let signature = secret_key.sign(&handover); + + prev_user + .verify(&handover, &signature) + .map_err(|_| FailedDelegationError::WrongSecretForUser(new_user.clone()))?; + + let mut new_delegations = self.delegations.clone(); + + new_delegations.push(Delegation::new(new_area, new_user, signature)); + + Ok(Self { + access_mode: self.access_mode.clone(), + namespace_key: self.namespace_key.clone(), + initial_authorisation: self.initial_authorisation.clone(), + user_key: self.user_key.clone(), + delegations: new_delegations, + }) + } + + /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. + pub async fn append_existing_delegation( + &mut self, + delegation: Delegation, + ) -> Result<(), InvalidDelegationError> { + let new_area = delegation.area(); + let new_user = delegation.user(); + let new_sig = delegation.signature(); + + if !self.granted_area().includes_area(new_area) { + return Err(InvalidDelegationError::AreaNotIncluded { + excluded_area: new_area.clone(), + claimed_receiver: new_user.clone(), + }); + } + + let handover = self.handover(new_area, new_user).await; + + let prev_receiver = self.receiver(); + + prev_receiver.verify(&handover, new_sig).map_err(|_| { + InvalidDelegationError::InvalidSignature { + claimed_receiver: new_user.clone(), + expected_signatory: prev_receiver.clone(), + signature: new_sig.clone(), + } + })?; + + self.delegations.push(delegation); + + Ok(()) + } + + /// The kind of access this capability grants. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_cap_mode) + pub fn access_mode(&self) -> &AccessMode { + &self.access_mode + } + + /// The user to whom this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_cap_receiver) + pub fn receiver(&self) -> &UserPublicKey { + if self.delegations.is_empty() { + return &self.user_key; + } + + // We can unwrap here because we know delegations isn't empty. + let last_delegation = self.delegations.last().unwrap(); + let receiver = last_delegation.user(); + + receiver + } + + /// The [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) for which this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_cap_granted_namespace) + pub fn granted_namespace(&self) -> &NamespacePublicKey { + &self.namespace_key + } + + /// The [`Area`] for which this capability grants access. + /// + /// [Definition](`https://willowprotocol.org/specs/meadowcap/index.html#owned_cap_granted_area`) + pub fn granted_area(&self) -> Area { + if self.delegations.is_empty() { + return Area::new_full(); + } + + // We can unwrap here because we know delegations isn't empty. + let last_delegation = self.delegations.last().unwrap(); + + last_delegation.area().clone() + } + + /// A bytestring to be signed for a new [`Delegation`]. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_handover) + // TODO: Make this sync. + async fn handover( + &self, + new_area: &Area, + new_user: &UserPublicKey, + ) -> Box<[u8]> { + let mut consumer = IntoVec::::new(); + + if self.delegations.is_empty() { + let prev_area = + Area::::new_subspace(self.user_key.clone()); + + // We can safely unwrap all these encodings as IntoVec's error is the never type. + new_area + .relative_encode(&prev_area, &mut consumer) + .await + .unwrap(); + self.initial_authorisation + .encode(&mut consumer) + .await + .unwrap(); + new_user.encode(&mut consumer).await.unwrap(); + + return consumer.into_vec().into(); + } + + // We can unwrap here because we know that self.delegations is not empty. + let last_delegation = self.delegations.last().unwrap(); + let prev_area = last_delegation.area(); + let prev_signature = last_delegation.signature(); + + // We can safely unwrap all these encodings as IntoVec's error is the never type. + new_area + .relative_encode(prev_area, &mut consumer) + .await + .unwrap(); + prev_signature.encode(&mut consumer).await.unwrap(); + new_user.encode(&mut consumer).await.unwrap(); + + consumer.into_vec().into() + } +} From 0ce9b64d4ec03ef54001772fd37c5eaaed82dc20 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Tue, 30 Jul 2024 19:06:18 +0100 Subject: [PATCH 11/27] McCapability --- meadowcap/src/communal_capability.rs | 19 ++-- meadowcap/src/lib.rs | 6 ++ meadowcap/src/mccapability.rs | 151 +++++++++++++++++++++++++++ meadowcap/src/owned_capability.rs | 26 ++++- 4 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 meadowcap/src/mccapability.rs diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs index a304dd4..994dfb4 100644 --- a/meadowcap/src/communal_capability.rs +++ b/meadowcap/src/communal_capability.rs @@ -6,7 +6,10 @@ use willow_data_model::{ parameters::{NamespaceId, SubspaceId}, }; -use crate::{AccessMode, Delegation, FailedDelegationError, InvalidDelegationError}; +use crate::{AccessMode, Delegation, FailedDelegationError, InvalidDelegationError, IsCommunal}; + +/// Returned when [`is_communal`](https://willowprotocol.org/specs/meadowcap/index.html#is_communal) unexpectedly mapped a given namespace to `false`. +pub struct NamespaceIsNotCommunalError(NamespacePublicKey); /// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). /// @@ -19,7 +22,7 @@ pub struct CommunalCapability< UserPublicKey, UserSignature, > where - NamespacePublicKey: NamespaceId + Encodable, + NamespacePublicKey: NamespaceId + Encodable + IsCommunal, UserPublicKey: SubspaceId + Encodable + Verifier, UserSignature: Encodable, { @@ -38,7 +41,7 @@ impl< UserSignature, > CommunalCapability where - NamespacePublicKey: NamespaceId + Encodable, + NamespacePublicKey: NamespaceId + Encodable + IsCommunal, UserPublicKey: SubspaceId + Encodable + Verifier, UserSignature: Encodable + Clone, { @@ -47,13 +50,17 @@ where namespace_key: NamespacePublicKey, user_key: UserPublicKey, access_mode: AccessMode, - ) -> Self { - Self { + ) -> Result> { + if !namespace_key.is_communal() { + return Err(NamespaceIsNotCommunalError(namespace_key.clone())); + } + + Ok(Self { access_mode, namespace_key, user_key, delegations: Vec::new(), - } + }) } /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 9d2aaf9..00c92aa 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -1,5 +1,10 @@ use willow_data_model::{grouping::area::Area, parameters::SubspaceId}; +/// Maps namespace public keys to booleans, determining whether that namespace of a particular [`willow_data_model::NamespaceId`] is [communal](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace) or [owned](https://willowprotocol.org/specs/meadowcap/index.html#owned_namespace). +pub trait IsCommunal { + fn is_communal(&self) -> bool; +} + /// A delegation of access rights to a user for a given area. #[derive(Clone)] pub struct Delegation< @@ -94,4 +99,5 @@ pub enum InvalidDelegationError< } pub mod communal_capability; +pub mod mccapability; pub mod owned_capability; diff --git a/meadowcap/src/mccapability.rs b/meadowcap/src/mccapability.rs new file mode 100644 index 0000000..95717d0 --- /dev/null +++ b/meadowcap/src/mccapability.rs @@ -0,0 +1,151 @@ +use signature::{Signer, Verifier}; +use willow_data_model::{ + encoding::parameters::Encodable, + grouping::area::Area, + parameters::{NamespaceId, SubspaceId}, +}; + +use crate::{ + communal_capability::{CommunalCapability, NamespaceIsNotCommunalError}, + owned_capability::{OwnedCapability, OwnedCapabilityCreationError}, + AccessMode, FailedDelegationError, IsCommunal, +}; + +/// A Meadowcap capability. +/// +/// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#Capability) +pub enum McCapability< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, +> where + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, + UserPublicKey: SubspaceId + Encodable + Verifier, + NamespaceSignature: Encodable + Clone, + UserSignature: Encodable + Clone, +{ + Communal(CommunalCapability), + Owned( + OwnedCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + >, + ), +} + +impl< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > + McCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > +where + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, + UserPublicKey: SubspaceId + Encodable + Verifier, + NamespaceSignature: Encodable + Clone, + UserSignature: Encodable + Clone, +{ + /// Create a new communal capability granting access to the [`SubspaceId`] corresponding to the given [`UserPublicKey`], or return an error if the namespace key is not communal. + pub fn new_communal( + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + access_mode: AccessMode, + ) -> Result> { + let cap = CommunalCapability::new(namespace_key, user_key, access_mode)?; + Ok(Self::Communal(cap)) + } + + /// Create a new owned capability granting access to the [full area](https://willowprotocol.org/specs/grouping-entries/index.html#full_area) of the [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) to the given [`UserPublicKey`]. + pub async fn new_owned( + namespace_key: NamespacePublicKey, + namespace_secret: NamespaceSecret, + user_key: UserPublicKey, + access_mode: AccessMode, + ) -> Result> + where + NamespaceSecret: Signer, + { + let cap = + OwnedCapability::new(namespace_key, namespace_secret, user_key, access_mode).await?; + + Ok(Self::Owned(cap)) + } + + /// The kind of access this capability grants. + pub fn access_mode(&self) -> &AccessMode { + match self { + Self::Communal(cap) => cap.access_mode(), + Self::Owned(cap) => cap.access_mode(), + } + } + + /// The user to whom this capability grants access. + pub fn receiver(&self) -> &UserPublicKey { + match self { + Self::Communal(cap) => cap.receiver(), + Self::Owned(cap) => cap.receiver(), + } + } + + /// The [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) for which this capability grants access. + pub fn granted_namespace(&self) -> &NamespacePublicKey { + match self { + Self::Communal(cap) => cap.granted_namespace(), + Self::Owned(cap) => cap.granted_namespace(), + } + } + + /// The [`Area`] for which this capability grants access. + pub fn granted_area(&self) -> Area { + match self { + Self::Communal(cap) => cap.granted_area(), + Self::Owned(cap) => cap.granted_area(), + } + } + + /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. + /// Will fail if the area is not included by this capability's granted area, or if the given secret key does not correspond to the capability's receiver. + pub async fn delegate( + &self, + secret_key: UserSecretKey, + new_user: UserPublicKey, + new_area: Area, + ) -> Result> + where + UserSecretKey: Signer, + { + let delegated = match self { + McCapability::Communal(cap) => { + let delegated = cap.delegate(secret_key, new_user, new_area).await?; + Self::Communal(delegated) + } + McCapability::Owned(cap) => { + let delegated = cap.delegate(secret_key, new_user, new_area).await?; + Self::Owned(delegated) + } + }; + + Ok(delegated) + } +} diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index 4626dbd..e5f1252 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -6,7 +6,15 @@ use willow_data_model::{ parameters::{NamespaceId, SubspaceId}, }; -use crate::{AccessMode, Delegation, FailedDelegationError, InvalidDelegationError}; +use crate::{AccessMode, Delegation, FailedDelegationError, InvalidDelegationError, IsCommunal}; + +/// Returned when an attempt to create a new owned capability failed. +pub enum OwnedCapabilityCreationError { + /// [`is_communal`](https://willowprotocol.org/specs/meadowcap/index.html#is_communal) unexpectedly mapped a given namespace to `true`. + NamespaceIsCommunal(NamespacePublicKey), + /// The resulting signature was faulty, probably due to the wrong secret being given. + InvalidSignature(SignatureError), +} /// A capability that implements [owned namespaces](https://willowprotocol.org/specs/meadowcap/index.html#owned_namespace). /// @@ -20,7 +28,7 @@ pub struct OwnedCapability< UserPublicKey, UserSignature, > where - NamespacePublicKey: NamespaceId + Encodable + Verifier, + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, UserPublicKey: SubspaceId + Encodable + Verifier, NamespaceSignature: Encodable, UserSignature: Encodable, @@ -51,7 +59,7 @@ impl< UserSignature, > where - NamespacePublicKey: NamespaceId + Encodable + Verifier, + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, UserPublicKey: SubspaceId + Encodable + Verifier, NamespaceSignature: Encodable + Clone, UserSignature: Encodable + Clone, @@ -62,10 +70,16 @@ where namespace_secret: NamespaceSecret, user_key: UserPublicKey, access_mode: AccessMode, - ) -> Result + ) -> Result> where NamespaceSecret: Signer, { + if namespace_key.is_communal() { + return Err(OwnedCapabilityCreationError::NamespaceIsCommunal( + namespace_key.clone(), + )); + } + let mut consumer = IntoVec::::new(); let access_byte = match access_mode { @@ -80,7 +94,9 @@ where let initial_authorisation = namespace_secret.sign(&message); - namespace_key.verify(&message, &initial_authorisation)?; + namespace_key + .verify(&message, &initial_authorisation) + .map_err(|err| OwnedCapabilityCreationError::InvalidSignature(err))?; Ok(Self { access_mode, From 0a4ae60bd8d9e2cfd7fbe192c8cd0d19f08caea3 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Wed, 31 Jul 2024 14:35:45 +0100 Subject: [PATCH 12/27] syncify meadowcap --- meadowcap/src/communal_capability.rs | 36 +++++++++++-------------- meadowcap/src/mccapability.rs | 8 +++--- meadowcap/src/owned_capability.rs | 39 +++++++++++----------------- 3 files changed, 34 insertions(+), 49 deletions(-) diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs index 994dfb4..adf16e5 100644 --- a/meadowcap/src/communal_capability.rs +++ b/meadowcap/src/communal_capability.rs @@ -1,7 +1,7 @@ use signature::{Signer, Verifier}; -use ufotofu::{local_nb::Consumer, sync::consumer::IntoVec}; +use ufotofu::{sync::consumer::IntoVec, sync::Consumer}; use willow_data_model::{ - encoding::{parameters::Encodable, relativity::RelativeEncodable}, + encoding::parameters_sync::{Encodable, RelativeEncodable}, grouping::area::Area, parameters::{NamespaceId, SubspaceId}, }; @@ -65,7 +65,7 @@ where /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). - pub async fn delegate( + pub fn delegate( &self, secret_key: UserSecretKey, new_user: UserPublicKey, @@ -85,7 +85,7 @@ where let prev_user = self.receiver(); - let handover = self.handover(&new_area, &new_user).await; + let handover = self.handover(&new_area, &new_user); let signature = secret_key.sign(&handover); prev_user @@ -105,7 +105,7 @@ where } /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. - pub async fn append_existing_delegation( + pub fn append_existing_delegation( &mut self, delegation: Delegation, ) -> Result<(), InvalidDelegationError> { @@ -120,7 +120,7 @@ where }); } - let handover = self.handover(new_area, new_user).await; + let handover = self.handover(new_area, new_user); let prev_receiver = self.receiver(); @@ -183,8 +183,8 @@ where /// A bytestring to be signed for a new [`Delegation`]. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_handover) - // TODO: Make this sync. - async fn handover( + + fn handover( &self, new_area: &Area, new_user: &UserPublicKey, @@ -201,13 +201,10 @@ where Area::::new_subspace(self.user_key.clone()); // We can safely unwrap all these encodings as IntoVec's error is the never type. - consumer.consume(first_byte).await.unwrap(); - self.namespace_key.encode(&mut consumer).await.unwrap(); - new_area - .relative_encode(&prev_area, &mut consumer) - .await - .unwrap(); - new_user.encode(&mut consumer).await.unwrap(); + consumer.consume(first_byte).unwrap(); + self.namespace_key.encode(&mut consumer).unwrap(); + new_area.relative_encode(&prev_area, &mut consumer).unwrap(); + new_user.encode(&mut consumer).unwrap(); return consumer.into_vec().into(); } @@ -218,12 +215,9 @@ where let prev_signature = last_delegation.signature(); // We can safely unwrap all these encodings as IntoVec's error is the never type. - new_area - .relative_encode(prev_area, &mut consumer) - .await - .unwrap(); - prev_signature.encode(&mut consumer).await.unwrap(); - new_user.encode(&mut consumer).await.unwrap(); + new_area.relative_encode(prev_area, &mut consumer).unwrap(); + prev_signature.encode(&mut consumer).unwrap(); + new_user.encode(&mut consumer).unwrap(); consumer.into_vec().into() } diff --git a/meadowcap/src/mccapability.rs b/meadowcap/src/mccapability.rs index 95717d0..d08df81 100644 --- a/meadowcap/src/mccapability.rs +++ b/meadowcap/src/mccapability.rs @@ -1,6 +1,6 @@ use signature::{Signer, Verifier}; use willow_data_model::{ - encoding::parameters::Encodable, + encoding::parameters_sync::Encodable, grouping::area::Area, parameters::{NamespaceId, SubspaceId}, }; @@ -126,7 +126,7 @@ where /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. /// Will fail if the area is not included by this capability's granted area, or if the given secret key does not correspond to the capability's receiver. - pub async fn delegate( + pub fn delegate( &self, secret_key: UserSecretKey, new_user: UserPublicKey, @@ -137,11 +137,11 @@ where { let delegated = match self { McCapability::Communal(cap) => { - let delegated = cap.delegate(secret_key, new_user, new_area).await?; + let delegated = cap.delegate(secret_key, new_user, new_area)?; Self::Communal(delegated) } McCapability::Owned(cap) => { - let delegated = cap.delegate(secret_key, new_user, new_area).await?; + let delegated = cap.delegate(secret_key, new_user, new_area)?; Self::Owned(delegated) } }; diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index e5f1252..5336e9c 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -1,7 +1,7 @@ use signature::{Error as SignatureError, Signer, Verifier}; -use ufotofu::{local_nb::Consumer, sync::consumer::IntoVec}; +use ufotofu::{sync::consumer::IntoVec, sync::Consumer}; use willow_data_model::{ - encoding::{parameters::Encodable, relativity::RelativeEncodable}, + encoding::parameters_sync::{Encodable, RelativeEncodable}, grouping::area::Area, parameters::{NamespaceId, SubspaceId}, }; @@ -87,8 +87,8 @@ where AccessMode::Write => 0x03, }; - consumer.consume(access_byte).await.unwrap(); - user_key.encode(&mut consumer).await.unwrap(); + consumer.consume(access_byte).unwrap(); + user_key.encode(&mut consumer).unwrap(); let message = consumer.into_vec(); @@ -109,7 +109,7 @@ where /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). - pub async fn delegate( + pub fn delegate( &self, secret_key: UserSecretKey, new_user: UserPublicKey, @@ -129,7 +129,7 @@ where let prev_user = self.receiver(); - let handover = self.handover(&new_area, &new_user).await; + let handover = self.handover(&new_area, &new_user); let signature = secret_key.sign(&handover); prev_user @@ -150,7 +150,7 @@ where } /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. - pub async fn append_existing_delegation( + pub fn append_existing_delegation( &mut self, delegation: Delegation, ) -> Result<(), InvalidDelegationError> { @@ -165,7 +165,7 @@ where }); } - let handover = self.handover(new_area, new_user).await; + let handover = self.handover(new_area, new_user); let prev_receiver = self.receiver(); @@ -229,7 +229,7 @@ where /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_handover) // TODO: Make this sync. - async fn handover( + fn handover( &self, new_area: &Area, new_user: &UserPublicKey, @@ -241,15 +241,9 @@ where Area::::new_subspace(self.user_key.clone()); // We can safely unwrap all these encodings as IntoVec's error is the never type. - new_area - .relative_encode(&prev_area, &mut consumer) - .await - .unwrap(); - self.initial_authorisation - .encode(&mut consumer) - .await - .unwrap(); - new_user.encode(&mut consumer).await.unwrap(); + new_area.relative_encode(&prev_area, &mut consumer).unwrap(); + self.initial_authorisation.encode(&mut consumer).unwrap(); + new_user.encode(&mut consumer).unwrap(); return consumer.into_vec().into(); } @@ -260,12 +254,9 @@ where let prev_signature = last_delegation.signature(); // We can safely unwrap all these encodings as IntoVec's error is the never type. - new_area - .relative_encode(prev_area, &mut consumer) - .await - .unwrap(); - prev_signature.encode(&mut consumer).await.unwrap(); - new_user.encode(&mut consumer).await.unwrap(); + new_area.relative_encode(prev_area, &mut consumer).unwrap(); + prev_signature.encode(&mut consumer).unwrap(); + new_user.encode(&mut consumer).unwrap(); consumer.into_vec().into() } From b4474ae8a72020a77a3fdc64dbdefc5e7c20fa56 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Wed, 31 Jul 2024 15:15:26 +0100 Subject: [PATCH 13/27] Add McAuthorisationToken --- data-model/src/parameters.rs | 1 + meadowcap/src/communal_capability.rs | 1 + meadowcap/src/lib.rs | 3 +- meadowcap/src/mc_authorisation_token.rs | 92 +++++++++++++++++++ .../src/{mccapability.rs => mc_capability.rs} | 73 ++++++++++++++- meadowcap/src/owned_capability.rs | 1 + 6 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 meadowcap/src/mc_authorisation_token.rs rename meadowcap/src/{mccapability.rs => mc_capability.rs} (70%) diff --git a/data-model/src/parameters.rs b/data-model/src/parameters.rs index eb48d53..98f9787 100644 --- a/data-model/src/parameters.rs +++ b/data-model/src/parameters.rs @@ -37,5 +37,6 @@ pub trait IsAuthorisedWrite< PD: PayloadDigest, > { + /// Determine whether this type (nominally a [`AuthorisationToken`](https://willowprotocol.org/specs/data-model/index.html#AuthorisationToken)) is able to prove write permission for a given [`Entry`]. fn is_authorised_write(&self, entry: &Entry) -> bool; } diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs index adf16e5..819125f 100644 --- a/meadowcap/src/communal_capability.rs +++ b/meadowcap/src/communal_capability.rs @@ -14,6 +14,7 @@ pub struct NamespaceIsNotCommunalError(NamespacePublicKey); /// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_capabilities). +#[derive(Clone)] pub struct CommunalCapability< const MCL: usize, const MCC: usize, diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 00c92aa..1ccc7ba 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -99,5 +99,6 @@ pub enum InvalidDelegationError< } pub mod communal_capability; -pub mod mccapability; +pub mod mc_authorisation_token; +pub mod mc_capability; pub mod owned_capability; diff --git a/meadowcap/src/mc_authorisation_token.rs b/meadowcap/src/mc_authorisation_token.rs new file mode 100644 index 0000000..39ac185 --- /dev/null +++ b/meadowcap/src/mc_authorisation_token.rs @@ -0,0 +1,92 @@ +use signature::Verifier; +use ufotofu::sync::consumer::IntoVec; +use willow_data_model::{ + encoding::parameters_sync::Encodable, + parameters::{IsAuthorisedWrite, NamespaceId, PayloadDigest, SubspaceId}, +}; + +use crate::{mc_capability::McCapability, IsCommunal}; + +/// To be used as an AuthorisationToken for Willow. +/// +/// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#MeadowcapAuthorisationToken) +pub struct McAuthorisationToken< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, +> where + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, + UserPublicKey: SubspaceId + Encodable + Verifier, + NamespaceSignature: Encodable + Clone, + UserSignature: Encodable + Clone, +{ + /// Certifies that an Entry may be written. + pub capability: McCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + >, + /// Proves that the [`willow_data_model::Entry`] was created by the [receiver](https://willowprotocol.org/specs/meadowcap/index.html#cap_receiver) of the [capability](https://willowprotocol.org/specs/meadowcap/index.html#mcat_cap). + pub signature: UserSignature, +} + +impl< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + PD, + > IsAuthorisedWrite + for McAuthorisationToken< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > +where + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, + UserPublicKey: SubspaceId + Encodable + Verifier, + NamespaceSignature: Encodable + Clone, + UserSignature: Encodable + Clone, + PD: PayloadDigest + Encodable, +{ + fn is_authorised_write( + &self, + entry: &willow_data_model::entry::Entry< + MCL, + MCC, + MPL, + NamespacePublicKey, + UserPublicKey, + PD, + >, + ) -> bool { + let mut consumer = IntoVec::::new(); + entry.encode(&mut consumer).unwrap(); + + if self + .capability + .receiver() + .verify(&consumer.into_vec(), &self.signature) + .is_err() + { + return false; + } + + true + } +} diff --git a/meadowcap/src/mccapability.rs b/meadowcap/src/mc_capability.rs similarity index 70% rename from meadowcap/src/mccapability.rs rename to meadowcap/src/mc_capability.rs index d08df81..0316036 100644 --- a/meadowcap/src/mccapability.rs +++ b/meadowcap/src/mc_capability.rs @@ -1,12 +1,15 @@ -use signature::{Signer, Verifier}; +use signature::{Error as SignatureError, Signer, Verifier}; +use ufotofu::sync::consumer::IntoVec; use willow_data_model::{ encoding::parameters_sync::Encodable, + entry::Entry, grouping::area::Area, - parameters::{NamespaceId, SubspaceId}, + parameters::{NamespaceId, PayloadDigest, SubspaceId}, }; use crate::{ communal_capability::{CommunalCapability, NamespaceIsNotCommunalError}, + mc_authorisation_token::McAuthorisationToken, owned_capability::{OwnedCapability, OwnedCapabilityCreationError}, AccessMode, FailedDelegationError, IsCommunal, }; @@ -14,6 +17,7 @@ use crate::{ /// A Meadowcap capability. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#Capability) +#[derive(Clone)] pub enum McCapability< const MCL: usize, const MCC: usize, @@ -148,4 +152,69 @@ where Ok(delegated) } + + /// Return a new AuthorisationToken without checking if the resulting signature is correct (e.g. because you are going to immediately do that by constructing an [`willow_data_model::AuthorisedEntry`]). + pub fn authorisation_token( + &self, + entry: Entry, + secret: UserSecret, + ) -> McAuthorisationToken< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > + where + UserSecret: Signer, + PD: PayloadDigest + Encodable, + { + let mut consumer = IntoVec::::new(); + entry.encode(&mut consumer).unwrap(); + + let signature = secret.sign(&consumer.into_vec()); + + McAuthorisationToken { + capability: self.clone(), + signature, + } + } + + /// Return a new [`AuthorisationToken`], or an error if the resulting signature was not for the capability's receiver. + pub fn authorisation_token_checked( + &self, + entry: Entry, + secret: UserSecret, + ) -> Result< + McAuthorisationToken< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + >, + SignatureError, + > + where + UserSecret: Signer, + PD: PayloadDigest + Encodable, + { + let mut consumer = IntoVec::::new(); + entry.encode(&mut consumer).unwrap(); + + let message = consumer.into_vec(); + + let signature = secret.sign(&message); + + self.receiver().verify(&message, &signature)?; + + Ok(McAuthorisationToken { + capability: self.clone(), + signature, + }) + } } diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index 5336e9c..d76b577 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -19,6 +19,7 @@ pub enum OwnedCapabilityCreationError { /// A capability that implements [owned namespaces](https://willowprotocol.org/specs/meadowcap/index.html#owned_namespace). /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_capabilities). +#[derive(Clone)] pub struct OwnedCapability< const MCL: usize, const MCC: usize, From 9f18a6c938f4eb4ec832c9dd55f099981c09f066 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Thu, 1 Aug 2024 14:22:24 +0100 Subject: [PATCH 14/27] Add Silly Signature scheme for fuzzing --- Cargo.lock | 3 ++ fuzz/Cargo.toml | 12 ++++++ fuzz/src/lib.rs | 1 + fuzz/src/silly_sigs.rs | 98 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 fuzz/src/silly_sigs.rs diff --git a/Cargo.lock b/Cargo.lock index ed26813..8d789f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,7 @@ checksum = "4958ec1997b05011d5c786bf4093cd48578bd9be2737350ab38659694083ddde" name = "meadowcap" version = "0.1.0" dependencies = [ + "arbitrary", "signature", "ufotofu", "willow-data-model", @@ -573,6 +574,8 @@ dependencies = [ "arbitrary", "earthstar", "libfuzzer-sys", + "meadowcap", + "signature", "smol", "ufotofu", "willow-data-model", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index d13024e..36e3a71 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -15,12 +15,17 @@ ufotofu = { version = "0.3.0", features=["dev"]} smol = "2.0.0" arbitrary = { version = "1.0.2", features = ["derive"] } libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } +signature = "2.2.0" [dependencies.willow-data-model] path = "../data-model" features = ["dev"] +[dependencies.meadowcap] +path = "../meadowcap" +features = ["dev"] + [dependencies.earthstar] path = "../earthstar" @@ -318,3 +323,10 @@ path = "fuzz_targets/range3d_rel_rang3d_encoding_random.rs" test = false doc = false bench = false + +[[bin]] +name = "mc_capability_delegation" +path = "fuzz_targets/mc_capability_delegation.rs" +test = false +doc = false +bench = false diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index a3877e7..72ff97e 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -1,3 +1,4 @@ pub mod encode; pub mod path; pub mod placeholder_params; +pub mod silly_sigs; diff --git a/fuzz/src/silly_sigs.rs b/fuzz/src/silly_sigs.rs new file mode 100644 index 0000000..2ade01f --- /dev/null +++ b/fuzz/src/silly_sigs.rs @@ -0,0 +1,98 @@ +use arbitrary::Arbitrary; +use meadowcap::IsCommunal; +use signature::{Error as SignatureError, Signer, Verifier}; +use willow_data_model::{ + encoding::parameters_sync::Encodable, + parameters::{NamespaceId, SubspaceId}, +}; + +/// A silly, trivial, insecure public key for fuzz testing. +#[derive(PartialEq, Eq, Debug, Arbitrary, Clone, Default, PartialOrd, Ord)] +pub struct SillyPublicKey(u8); + +/// A silly, trivial, insecure secret key for fuzz testing. +/// The corresponding [`SillyPublicKey`] is the identity of the secret. +#[derive(PartialEq, Eq, Debug, Arbitrary)] +pub struct SillySecret(u8); + +impl SillySecret { + pub fn corresponding_public_key(&self) -> SillyPublicKey { + SillyPublicKey(self.0) + } +} + +/// A silly, trivial, insecure keypair for fuzz testing. +/// The [`SillyPublicKey`]'s member must be the same as its [`SillySecret`] to be a valid keypair. +#[derive(PartialEq, Eq, Debug, Arbitrary)] +pub struct SillyKeypair(pub SillyPublicKey, pub SillySecret); + +/// A silly, trivial, insecure signature for fuzz testing. +/// It's the public key followed by the message itself. +#[derive(PartialEq, Eq, Debug, Arbitrary, Clone)] +pub struct SillySig(SillyPublicKey, Box<[u8]>); + +impl Signer for SillySecret { + fn try_sign(&self, msg: &[u8]) -> Result { + Ok(SillySig(SillyPublicKey(self.0), Box::from(msg))) + } +} + +impl Verifier for SillyPublicKey { + fn verify(&self, msg: &[u8], signature: &SillySig) -> Result<(), SignatureError> { + if &signature.0 != self { + return Err(SignatureError::new()); + } + + let sig_msg = signature.1.as_ref(); + + if msg != sig_msg { + return Err(SignatureError::new()); + } + + Ok(()) + } +} + +impl NamespaceId for SillyPublicKey {} + +impl SubspaceId for SillyPublicKey { + fn successor(&self) -> Option { + if self.0 == 255 { + return None; + } + + Some(SillyPublicKey(self.0 + 1)) + } +} + +impl IsCommunal for SillyPublicKey { + fn is_communal(&self) -> bool { + self.0 % 2 == 0 + } +} + +impl Encodable for SillyPublicKey { + fn encode(&self, consumer: &mut Consumer) -> Result<(), Consumer::Error> + where + Consumer: ufotofu::sync::BulkConsumer, + { + consumer.consume(self.0)?; + + Ok(()) + } +} + +impl Encodable for SillySig { + fn encode(&self, consumer: &mut Consumer) -> Result<(), Consumer::Error> + where + Consumer: ufotofu::sync::BulkConsumer, + { + self.0.encode(consumer)?; + + consumer + .bulk_consume_full_slice(self.1.as_ref()) + .map_err(|err| err.reason)?; + + Ok(()) + } +} From 727d340169ac5f9bf4c93ac25a749357de114656 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Thu, 1 Aug 2024 14:22:32 +0100 Subject: [PATCH 15/27] Delegation fuzz tests --- fuzz/fuzz_targets/mc_capability_delegation.rs | 41 ++++++++ meadowcap/Cargo.toml | 5 + meadowcap/src/communal_capability.rs | 55 +++++++++-- meadowcap/src/lib.rs | 20 +++- meadowcap/src/mc_capability.rs | 73 ++++++++++++-- meadowcap/src/owned_capability.rs | 98 ++++++++++++++++--- 6 files changed, 258 insertions(+), 34 deletions(-) create mode 100644 fuzz/fuzz_targets/mc_capability_delegation.rs diff --git a/fuzz/fuzz_targets/mc_capability_delegation.rs b/fuzz/fuzz_targets/mc_capability_delegation.rs new file mode 100644 index 0000000..a278385 --- /dev/null +++ b/fuzz/fuzz_targets/mc_capability_delegation.rs @@ -0,0 +1,41 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::{mc_capability::McCapability, FailedDelegationError}; +use willow_data_model::grouping::area::Area; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySecret, SillySig}; + +fuzz_target!(|data: ( + SillySecret, + SillyPublicKey, + Area<16, 16, 16, SillyPublicKey>, + McCapability<16, 16, 16, SillyPublicKey, SillySig, SillyPublicKey, SillySig> +)| { + let (secret, new_user, area, mc_cap) = data; + + let area_is_included = mc_cap.granted_area().includes_area(&area); + let is_correct_secret = mc_cap.receiver() == &secret.corresponding_public_key(); + + match mc_cap.delegate(&secret, &new_user, &area) { + Ok(delegated_cap) => { + assert!(area_is_included); + assert!(is_correct_secret); + + assert_eq!(delegated_cap.granted_area(), area); + assert_eq!(delegated_cap.receiver(), &new_user); + } + Err(err) => match err { + FailedDelegationError::AreaNotIncluded { + excluded_area, + claimed_receiver: _, + } => { + assert!(!area_is_included); + assert_eq!(excluded_area, area); + } + FailedDelegationError::WrongSecretForUser(pub_key) => { + assert_eq!(&pub_key, mc_cap.receiver()); + assert!(secret.corresponding_public_key() != pub_key) + } + }, + } +}); diff --git a/meadowcap/Cargo.toml b/meadowcap/Cargo.toml index 486e95f..28692bd 100644 --- a/meadowcap/Cargo.toml +++ b/meadowcap/Cargo.toml @@ -3,9 +3,14 @@ name = "meadowcap" version = "0.1.0" edition = "2021" +[features] +default = [] +dev = ["dep:arbitrary"] + [dependencies] signature = "2.2.0" ufotofu = "0.3.0" +arbitrary = { version = "1.0.2", features = ["derive"], optional = true } [dependencies.willow-data-model] path = "../data-model" diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs index 819125f..21aa084 100644 --- a/meadowcap/src/communal_capability.rs +++ b/meadowcap/src/communal_capability.rs @@ -14,7 +14,7 @@ pub struct NamespaceIsNotCommunalError(NamespacePublicKey); /// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_capabilities). -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct CommunalCapability< const MCL: usize, const MCC: usize, @@ -68,34 +68,38 @@ where /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). pub fn delegate( &self, - secret_key: UserSecretKey, - new_user: UserPublicKey, - new_area: Area, + secret_key: &UserSecretKey, + new_user: &UserPublicKey, + new_area: &Area, ) -> Result> where UserSecretKey: Signer, { let prev_area = self.granted_area(); - if !prev_area.includes_area(&new_area) { + if !prev_area.includes_area(new_area) { return Err(FailedDelegationError::AreaNotIncluded { - excluded_area: new_area, - claimed_receiver: new_user, + excluded_area: new_area.clone(), + claimed_receiver: new_user.clone(), }); } let prev_user = self.receiver(); - let handover = self.handover(&new_area, &new_user); + let handover = self.handover(new_area, new_user); let signature = secret_key.sign(&handover); prev_user .verify(&handover, &signature) - .map_err(|_| FailedDelegationError::WrongSecretForUser(new_user.clone()))?; + .map_err(|_| FailedDelegationError::WrongSecretForUser(self.receiver().clone()))?; let mut new_delegations = self.delegations.clone(); - new_delegations.push(Delegation::new(new_area, new_user, signature)); + new_delegations.push(Delegation::new( + new_area.clone(), + new_user.clone(), + signature, + )); Ok(Self { access_mode: self.access_mode.clone(), @@ -223,3 +227,34 @@ where consumer.into_vec().into() } } + +#[cfg(feature = "dev")] +use arbitrary::{Arbitrary, Error as ArbitraryError}; + +#[cfg(feature = "dev")] +impl< + 'a, + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + UserPublicKey, + UserSignature, + > Arbitrary<'a> + for CommunalCapability +where + NamespacePublicKey: NamespaceId + Encodable + IsCommunal + Arbitrary<'a>, + UserPublicKey: SubspaceId + Encodable + Verifier + Arbitrary<'a>, + UserSignature: Encodable + Clone, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let namespace_key: NamespacePublicKey = Arbitrary::arbitrary(u)?; + let user_key: UserPublicKey = Arbitrary::arbitrary(u)?; + let access_mode: AccessMode = Arbitrary::arbitrary(u)?; + + match Self::new(namespace_key, user_key, access_mode) { + Ok(cap) => Ok(cap), + Err(_) => Err(ArbitraryError::IncorrectFormat), + } + } +} diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 1ccc7ba..0c9268a 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -6,7 +6,7 @@ pub trait IsCommunal { } /// A delegation of access rights to a user for a given area. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Delegation< const MCL: usize, const MCC: usize, @@ -55,12 +55,28 @@ where } /// A mode granting read or write access to some [`Area`]. -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum AccessMode { Read, Write, } +#[cfg(feature = "dev")] +use arbitrary::Arbitrary; + +#[cfg(feature = "dev")] +impl<'a> Arbitrary<'a> for AccessMode { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let read: bool = Arbitrary::arbitrary(u)?; + + if read { + Ok(Self::Read) + } else { + Ok(Self::Write) + } + } +} + /// Returned when an attempt to delegate a capability failed. pub enum FailedDelegationError< const MCL: usize, diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index 0316036..dff9118 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -17,7 +17,7 @@ use crate::{ /// A Meadowcap capability. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#Capability) -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum McCapability< const MCL: usize, const MCC: usize, @@ -90,8 +90,7 @@ where where NamespaceSecret: Signer, { - let cap = - OwnedCapability::new(namespace_key, namespace_secret, user_key, access_mode).await?; + let cap = OwnedCapability::new(namespace_key, namespace_secret, user_key, access_mode)?; Ok(Self::Owned(cap)) } @@ -132,9 +131,9 @@ where /// Will fail if the area is not included by this capability's granted area, or if the given secret key does not correspond to the capability's receiver. pub fn delegate( &self, - secret_key: UserSecretKey, - new_user: UserPublicKey, - new_area: Area, + secret_key: &UserSecretKey, + new_user: &UserPublicKey, + new_area: &Area, ) -> Result> where UserSecretKey: Signer, @@ -142,10 +141,12 @@ where let delegated = match self { McCapability::Communal(cap) => { let delegated = cap.delegate(secret_key, new_user, new_area)?; + Self::Communal(delegated) } McCapability::Owned(cap) => { let delegated = cap.delegate(secret_key, new_user, new_area)?; + Self::Owned(delegated) } }; @@ -218,3 +219,63 @@ where }) } } + +#[cfg(feature = "dev")] +use arbitrary::Arbitrary; + +#[cfg(feature = "dev")] +impl< + 'a, + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > Arbitrary<'a> + for McCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > +where + NamespacePublicKey: + NamespaceId + Encodable + IsCommunal + Arbitrary<'a> + Verifier, + UserPublicKey: SubspaceId + Encodable + Verifier + Arbitrary<'a>, + NamespaceSignature: Encodable + Clone + Arbitrary<'a>, + UserSignature: Encodable + Clone, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let is_communal: bool = Arbitrary::arbitrary(u)?; + + if is_communal { + let cap: CommunalCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + UserPublicKey, + UserSignature, + > = Arbitrary::arbitrary(u)?; + + return Ok(Self::Communal(cap)); + } + + let cap: OwnedCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > = Arbitrary::arbitrary(u)?; + + Ok(Self::Owned(cap)) + } +} diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index d76b577..4f56432 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -19,7 +19,7 @@ pub enum OwnedCapabilityCreationError { /// A capability that implements [owned namespaces](https://willowprotocol.org/specs/meadowcap/index.html#owned_namespace). /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_capabilities). -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct OwnedCapability< const MCL: usize, const MCC: usize, @@ -30,8 +30,8 @@ pub struct OwnedCapability< UserSignature, > where NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, - UserPublicKey: SubspaceId + Encodable + Verifier, NamespaceSignature: Encodable, + UserPublicKey: SubspaceId + Encodable + Verifier, UserSignature: Encodable, { access_mode: AccessMode, @@ -61,12 +61,12 @@ impl< > where NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, - UserPublicKey: SubspaceId + Encodable + Verifier, NamespaceSignature: Encodable + Clone, + UserPublicKey: SubspaceId + Encodable + Verifier, UserSignature: Encodable + Clone, { /// Create a new owned capability granting access to the [full area](https://willowprotocol.org/specs/grouping-entries/index.html#full_area) of the [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) to the given [`UserPublicKey`]. - pub async fn new( + pub fn new( namespace_key: NamespacePublicKey, namespace_secret: NamespaceSecret, user_key: UserPublicKey, @@ -112,34 +112,38 @@ where /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). pub fn delegate( &self, - secret_key: UserSecretKey, - new_user: UserPublicKey, - new_area: Area, + secret_key: &UserSecretKey, + new_user: &UserPublicKey, + new_area: &Area, ) -> Result> where UserSecretKey: Signer, { let prev_area = self.granted_area(); - if !prev_area.includes_area(&new_area) { + if !prev_area.includes_area(new_area) { return Err(FailedDelegationError::AreaNotIncluded { - excluded_area: new_area, - claimed_receiver: new_user, + excluded_area: new_area.clone(), + claimed_receiver: new_user.clone(), }); } let prev_user = self.receiver(); - let handover = self.handover(&new_area, &new_user); + let handover = self.handover(new_area, new_user); let signature = secret_key.sign(&handover); prev_user .verify(&handover, &signature) - .map_err(|_| FailedDelegationError::WrongSecretForUser(new_user.clone()))?; + .map_err(|_| FailedDelegationError::WrongSecretForUser(self.receiver().clone()))?; let mut new_delegations = self.delegations.clone(); - new_delegations.push(Delegation::new(new_area, new_user, signature)); + new_delegations.push(Delegation::new( + new_area.clone(), + new_user.clone(), + signature, + )); Ok(Self { access_mode: self.access_mode.clone(), @@ -229,7 +233,6 @@ where /// A bytestring to be signed for a new [`Delegation`]. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_handover) - // TODO: Make this sync. fn handover( &self, new_area: &Area, @@ -238,8 +241,7 @@ where let mut consumer = IntoVec::::new(); if self.delegations.is_empty() { - let prev_area = - Area::::new_subspace(self.user_key.clone()); + let prev_area = Area::::new_full(); // We can safely unwrap all these encodings as IntoVec's error is the never type. new_area.relative_encode(&prev_area, &mut consumer).unwrap(); @@ -262,3 +264,67 @@ where consumer.into_vec().into() } } + +#[cfg(feature = "dev")] +use arbitrary::{Arbitrary, Error as ArbitraryError}; + +#[cfg(feature = "dev")] +impl< + 'a, + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > Arbitrary<'a> + for OwnedCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > +where + NamespacePublicKey: + NamespaceId + Encodable + IsCommunal + Arbitrary<'a> + Verifier, + UserPublicKey: SubspaceId + Encodable + Verifier + Arbitrary<'a>, + NamespaceSignature: Encodable + Clone + Arbitrary<'a>, + UserSignature: Encodable + Clone, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let namespace_key: NamespacePublicKey = Arbitrary::arbitrary(u)?; + + let initial_authorisation: NamespaceSignature = Arbitrary::arbitrary(u)?; + + let user_key: UserPublicKey = Arbitrary::arbitrary(u)?; + let access_mode: AccessMode = Arbitrary::arbitrary(u)?; + + let mut consumer = IntoVec::::new(); + + let access_byte = match access_mode { + AccessMode::Read => 0x02, + AccessMode::Write => 0x03, + }; + + consumer.consume(access_byte).unwrap(); + user_key.encode(&mut consumer).unwrap(); + + let message = consumer.into_vec(); + + namespace_key + .verify(&message, &initial_authorisation) + .map_err(|_| ArbitraryError::IncorrectFormat)?; + + Ok(Self { + access_mode, + initial_authorisation, + namespace_key, + user_key, + delegations: Vec::new(), + }) + } +} From a4c6105206b31f7416484862c857aaa3cde9963c Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Thu, 1 Aug 2024 16:42:08 +0100 Subject: [PATCH 16/27] Add fuzz test for appending delegations --- fuzz/Cargo.toml | 7 ++ .../mc_capability_append_delegation.rs | 92 +++++++++++++++++++ fuzz/fuzz_targets/mc_capability_delegation.rs | 28 ++++-- fuzz/src/silly_sigs.rs | 6 ++ meadowcap/src/communal_capability.rs | 7 +- meadowcap/src/lib.rs | 31 ++++++- meadowcap/src/mc_capability.rs | 23 ++++- meadowcap/src/owned_capability.rs | 7 +- 8 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 fuzz/fuzz_targets/mc_capability_append_delegation.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 36e3a71..0313ab0 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -330,3 +330,10 @@ path = "fuzz_targets/mc_capability_delegation.rs" test = false doc = false bench = false + +[[bin]] +name = "mc_capability_append_delegation" +path = "fuzz_targets/mc_capability_append_delegation.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/mc_capability_append_delegation.rs b/fuzz/fuzz_targets/mc_capability_append_delegation.rs new file mode 100644 index 0000000..8ea693b --- /dev/null +++ b/fuzz/fuzz_targets/mc_capability_append_delegation.rs @@ -0,0 +1,92 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::{mc_capability::McCapability, Delegation, InvalidDelegationError}; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; + +fuzz_target!(|data: ( + Delegation<16, 16, 16, SillyPublicKey, SillySig>, + McCapability<16, 16, 16, SillyPublicKey, SillySig, SillyPublicKey, SillySig>, + Vec +)| { + let (delegation, mc_cap, delegees) = data; + + let mut mut_cap = mc_cap.clone(); + + let mut last_receiver = mut_cap.receiver().clone(); + let granted_area = mut_cap.granted_area(); + + for delegee in delegees { + mut_cap = mut_cap + .delegate( + &last_receiver.corresponding_secret_key(), + &delegee, + &granted_area, + ) + .unwrap(); + last_receiver = delegee; + } + + let claimed_area = delegation.area().clone(); + let delegation_user = delegation.user().clone(); + let delegation_sig = delegation.signature().clone(); + + let granted_area_includes_delegation = mut_cap.granted_area().includes_area(delegation.area()); + + let actual_receiver_secret = mc_cap.receiver().corresponding_secret_key(); + + match mut_cap.append_existing_delegation(delegation) { + Ok(_) => { + println!("yay"); + assert!(granted_area_includes_delegation); + + // Because there is only one user who can delegate a given capability, we know what it should look like given the same new_area and new_user. + let expected_cap = + mc_cap.delegate(&actual_receiver_secret, &delegation_user, &claimed_area); + + match expected_cap { + Ok(cap) => { + assert_eq!(cap, mut_cap); + } + Err(_) => { + panic!("The delegation should not have been possible") + } + } + } + Err(err) => match err { + InvalidDelegationError::AreaNotIncluded { + excluded_area, + claimed_receiver: _, + } => { + assert!(!granted_area_includes_delegation); + assert_eq!(excluded_area, claimed_area); + } + InvalidDelegationError::InvalidSignature { + expected_signatory, + claimed_receiver, + signature: _, + } => { + assert_eq!(&expected_signatory, mut_cap.receiver()); + assert_eq!(delegation_user, claimed_receiver); + + // Because there is only one user who can delegate a given capability, we know what it should look like given the same new_area and new_user. + let expected_cap = + mc_cap.delegate(&actual_receiver_secret, &delegation_user, &claimed_area); + + match expected_cap { + Ok(cap) => match cap.delegations().last() { + Some(valid_delegation) => { + assert!(valid_delegation.signature() != &delegation_sig) + } + None => { + unreachable!( + "We just made a delegation, this really should not happen!" + ) + } + }, + Err(_) => panic!("The expected cap should have been fine..."), + } + } + }, + } +}); diff --git a/fuzz/fuzz_targets/mc_capability_delegation.rs b/fuzz/fuzz_targets/mc_capability_delegation.rs index a278385..025d6ce 100644 --- a/fuzz/fuzz_targets/mc_capability_delegation.rs +++ b/fuzz/fuzz_targets/mc_capability_delegation.rs @@ -9,14 +9,30 @@ fuzz_target!(|data: ( SillySecret, SillyPublicKey, Area<16, 16, 16, SillyPublicKey>, - McCapability<16, 16, 16, SillyPublicKey, SillySig, SillyPublicKey, SillySig> + McCapability<16, 16, 16, SillyPublicKey, SillySig, SillyPublicKey, SillySig>, + Vec )| { - let (secret, new_user, area, mc_cap) = data; + let (secret, new_user, area, mc_cap, delegees) = data; - let area_is_included = mc_cap.granted_area().includes_area(&area); - let is_correct_secret = mc_cap.receiver() == &secret.corresponding_public_key(); + let mut cap_with_delegees = mc_cap.clone(); + let mut last_receiver = mc_cap.receiver().clone(); + let granted_area = mc_cap.granted_area(); - match mc_cap.delegate(&secret, &new_user, &area) { + for delegee in delegees { + cap_with_delegees = cap_with_delegees + .delegate( + &last_receiver.corresponding_secret_key(), + &delegee, + &granted_area, + ) + .unwrap(); + last_receiver = delegee; + } + + let area_is_included = cap_with_delegees.granted_area().includes_area(&area); + let is_correct_secret = cap_with_delegees.receiver() == &secret.corresponding_public_key(); + + match cap_with_delegees.delegate(&secret, &new_user, &area) { Ok(delegated_cap) => { assert!(area_is_included); assert!(is_correct_secret); @@ -33,7 +49,7 @@ fuzz_target!(|data: ( assert_eq!(excluded_area, area); } FailedDelegationError::WrongSecretForUser(pub_key) => { - assert_eq!(&pub_key, mc_cap.receiver()); + assert_eq!(&pub_key, cap_with_delegees.receiver()); assert!(secret.corresponding_public_key() != pub_key) } }, diff --git a/fuzz/src/silly_sigs.rs b/fuzz/src/silly_sigs.rs index 2ade01f..c2faa9b 100644 --- a/fuzz/src/silly_sigs.rs +++ b/fuzz/src/silly_sigs.rs @@ -10,6 +10,12 @@ use willow_data_model::{ #[derive(PartialEq, Eq, Debug, Arbitrary, Clone, Default, PartialOrd, Ord)] pub struct SillyPublicKey(u8); +impl SillyPublicKey { + pub fn corresponding_secret_key(&self) -> SillySecret { + SillySecret(self.0) + } +} + /// A silly, trivial, insecure secret key for fuzz testing. /// The corresponding [`SillyPublicKey`] is the identity of the secret. #[derive(PartialEq, Eq, Debug, Arbitrary)] diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs index 21aa084..29379ca 100644 --- a/meadowcap/src/communal_capability.rs +++ b/meadowcap/src/communal_capability.rs @@ -14,7 +14,7 @@ pub struct NamespaceIsNotCommunalError(NamespacePublicKey); /// A capability that implements [communal namespaces](https://willowprotocol.org/specs/meadowcap/index.html#communal_namespace). /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_capabilities). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct CommunalCapability< const MCL: usize, const MCC: usize, @@ -185,6 +185,11 @@ where last_delegation.area().clone() } + /// Return a slice of all [`Delegation`]s made to this capability. + pub fn delegations(&self) -> &[Delegation] { + &self.delegations + } + /// A bytestring to be signed for a new [`Delegation`]. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_handover) diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 0c9268a..47055a9 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -6,7 +6,7 @@ pub trait IsCommunal { } /// A delegation of access rights to a user for a given area. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Delegation< const MCL: usize, const MCC: usize, @@ -54,16 +54,36 @@ where } } +#[cfg(feature = "dev")] +use arbitrary::Arbitrary; + +#[cfg(feature = "dev")] +impl<'a, const MCL: usize, const MCC: usize, const MPL: usize, UserPublicKey, UserSignature> + Arbitrary<'a> for Delegation +where + UserSignature: Arbitrary<'a>, + UserPublicKey: SubspaceId + Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let area: Area = Arbitrary::arbitrary(u)?; + let user: UserPublicKey = Arbitrary::arbitrary(u)?; + let signature: UserSignature = Arbitrary::arbitrary(u)?; + + Ok(Self { + area, + signature, + user, + }) + } +} + /// A mode granting read or write access to some [`Area`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum AccessMode { Read, Write, } -#[cfg(feature = "dev")] -use arbitrary::Arbitrary; - #[cfg(feature = "dev")] impl<'a> Arbitrary<'a> for AccessMode { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { @@ -78,6 +98,7 @@ impl<'a> Arbitrary<'a> for AccessMode { } /// Returned when an attempt to delegate a capability failed. +#[derive(Debug)] pub enum FailedDelegationError< const MCL: usize, const MCC: usize, diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index dff9118..dbf960a 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -11,13 +11,13 @@ use crate::{ communal_capability::{CommunalCapability, NamespaceIsNotCommunalError}, mc_authorisation_token::McAuthorisationToken, owned_capability::{OwnedCapability, OwnedCapabilityCreationError}, - AccessMode, FailedDelegationError, IsCommunal, + AccessMode, Delegation, FailedDelegationError, InvalidDelegationError, IsCommunal, }; /// A Meadowcap capability. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#Capability) -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum McCapability< const MCL: usize, const MCC: usize, @@ -127,6 +127,14 @@ where } } + /// Return a slice of all [`Delegation`]s made to this capability. + pub fn delegations(&self) -> &[Delegation] { + match self { + McCapability::Communal(cap) => cap.delegations(), + McCapability::Owned(cap) => cap.delegations(), + } + } + /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. /// Will fail if the area is not included by this capability's granted area, or if the given secret key does not correspond to the capability's receiver. pub fn delegate( @@ -154,6 +162,17 @@ where Ok(delegated) } + /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. + pub fn append_existing_delegation( + &mut self, + delegation: Delegation, + ) -> Result<(), InvalidDelegationError> { + match self { + McCapability::Communal(cap) => cap.append_existing_delegation(delegation), + McCapability::Owned(cap) => cap.append_existing_delegation(delegation), + } + } + /// Return a new AuthorisationToken without checking if the resulting signature is correct (e.g. because you are going to immediately do that by constructing an [`willow_data_model::AuthorisedEntry`]). pub fn authorisation_token( &self, diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index 4f56432..0181e82 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -19,7 +19,7 @@ pub enum OwnedCapabilityCreationError { /// A capability that implements [owned namespaces](https://willowprotocol.org/specs/meadowcap/index.html#owned_namespace). /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_capabilities). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct OwnedCapability< const MCL: usize, const MCC: usize, @@ -230,6 +230,11 @@ where last_delegation.area().clone() } + /// Return a slice of all [`Delegation`]s made to this capability. + pub fn delegations(&self) -> &[Delegation] { + &self.delegations + } + /// A bytestring to be signed for a new [`Delegation`]. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_handover) From ea11ea4e6eac4e561a4f7bc7e6981cfd6cde296b Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Fri, 2 Aug 2024 11:07:51 +0100 Subject: [PATCH 17/27] Even Sillier Signature Scheme --- .../mc_capability_append_delegation.rs | 28 +++++++++---------- fuzz/src/silly_sigs.rs | 20 ++++++------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/fuzz/fuzz_targets/mc_capability_append_delegation.rs b/fuzz/fuzz_targets/mc_capability_append_delegation.rs index 8ea693b..1d2402a 100644 --- a/fuzz/fuzz_targets/mc_capability_append_delegation.rs +++ b/fuzz/fuzz_targets/mc_capability_append_delegation.rs @@ -33,16 +33,20 @@ fuzz_target!(|data: ( let granted_area_includes_delegation = mut_cap.granted_area().includes_area(delegation.area()); - let actual_receiver_secret = mc_cap.receiver().corresponding_secret_key(); + let actual_receiver_secret = mut_cap.receiver().corresponding_secret_key(); + + let cap_before_delegation = mut_cap.clone(); match mut_cap.append_existing_delegation(delegation) { Ok(_) => { - println!("yay"); assert!(granted_area_includes_delegation); // Because there is only one user who can delegate a given capability, we know what it should look like given the same new_area and new_user. - let expected_cap = - mc_cap.delegate(&actual_receiver_secret, &delegation_user, &claimed_area); + let expected_cap = cap_before_delegation.delegate( + &actual_receiver_secret, + &delegation_user, + &claimed_area, + ); match expected_cap { Ok(cap) => { @@ -71,19 +75,13 @@ fuzz_target!(|data: ( // Because there is only one user who can delegate a given capability, we know what it should look like given the same new_area and new_user. let expected_cap = - mc_cap.delegate(&actual_receiver_secret, &delegation_user, &claimed_area); + mut_cap.delegate(&actual_receiver_secret, &delegation_user, &claimed_area); match expected_cap { - Ok(cap) => match cap.delegations().last() { - Some(valid_delegation) => { - assert!(valid_delegation.signature() != &delegation_sig) - } - None => { - unreachable!( - "We just made a delegation, this really should not happen!" - ) - } - }, + Ok(cap) => { + let valid_delegation = cap.delegations().last().unwrap(); + assert!(valid_delegation.signature() != &delegation_sig); + } Err(_) => panic!("The expected cap should have been fine..."), } } diff --git a/fuzz/src/silly_sigs.rs b/fuzz/src/silly_sigs.rs index c2faa9b..e0a0865 100644 --- a/fuzz/src/silly_sigs.rs +++ b/fuzz/src/silly_sigs.rs @@ -35,23 +35,23 @@ pub struct SillyKeypair(pub SillyPublicKey, pub SillySecret); /// A silly, trivial, insecure signature for fuzz testing. /// It's the public key followed by the message itself. #[derive(PartialEq, Eq, Debug, Arbitrary, Clone)] -pub struct SillySig(SillyPublicKey, Box<[u8]>); +pub struct SillySig(u8); impl Signer for SillySecret { fn try_sign(&self, msg: &[u8]) -> Result { - Ok(SillySig(SillyPublicKey(self.0), Box::from(msg))) + let first_msg_byte = msg.first().unwrap_or(&0x00); + + Ok(SillySig(self.0 ^ first_msg_byte)) } } impl Verifier for SillyPublicKey { fn verify(&self, msg: &[u8], signature: &SillySig) -> Result<(), SignatureError> { - if &signature.0 != self { - return Err(SignatureError::new()); - } + let first_msg_byte = msg.first().unwrap_or(&0x00); - let sig_msg = signature.1.as_ref(); + let expected_sig = self.0 ^ first_msg_byte; - if msg != sig_msg { + if signature.0 != expected_sig { return Err(SignatureError::new()); } @@ -93,11 +93,7 @@ impl Encodable for SillySig { where Consumer: ufotofu::sync::BulkConsumer, { - self.0.encode(consumer)?; - - consumer - .bulk_consume_full_slice(self.1.as_ref()) - .map_err(|err| err.reason)?; + consumer.consume(self.0)?; Ok(()) } From 69773ae519f0d6e935899780d0b6c24db890e74b Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Fri, 2 Aug 2024 12:21:54 +0100 Subject: [PATCH 18/27] McAuthorisationToken improvements + fuzz tests --- Cargo.lock | 1 + fuzz/Cargo.toml | 7 ++ fuzz/fuzz_targets/mc_is_authorised_write.rs | 41 ++++++++ fuzz/src/placeholder_params.rs | 28 +++++ meadowcap/Cargo.toml | 1 + meadowcap/src/mc_authorisation_token.rs | 62 ++++++++++- meadowcap/src/mc_capability.rs | 108 +++++++++++++++----- 7 files changed, 223 insertions(+), 25 deletions(-) create mode 100644 fuzz/fuzz_targets/mc_is_authorised_write.rs diff --git a/Cargo.lock b/Cargo.lock index 8d789f0..f49c64f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,7 @@ name = "meadowcap" version = "0.1.0" dependencies = [ "arbitrary", + "either", "signature", "ufotofu", "willow-data-model", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 0313ab0..f2b8c1d 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -337,3 +337,10 @@ path = "fuzz_targets/mc_capability_append_delegation.rs" test = false doc = false bench = false + +[[bin]] +name = "mc_is_authorised_write" +path = "fuzz_targets/mc_is_authorised_write.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/mc_is_authorised_write.rs b/fuzz/fuzz_targets/mc_is_authorised_write.rs new file mode 100644 index 0000000..015b3b6 --- /dev/null +++ b/fuzz/fuzz_targets/mc_is_authorised_write.rs @@ -0,0 +1,41 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::{mc_authorisation_token::McAuthorisationToken, AccessMode}; +use signature::Signer; +use ufotofu::sync::consumer::IntoVec; +use willow_data_model::encoding::parameters_sync::Encodable; +use willow_data_model::entry::Entry; +use willow_data_model::parameters::IsAuthorisedWrite; +use willow_data_model_fuzz::{ + placeholder_params::FakePayloadDigest, + silly_sigs::{SillyPublicKey, SillySig}, +}; + +fuzz_target!(|data: ( + Entry<16, 16, 16, SillyPublicKey, SillyPublicKey, FakePayloadDigest>, + McAuthorisationToken<16, 16, 16, SillyPublicKey, SillySig, SillyPublicKey, SillySig> +)| { + let (entry, token) = data; + + let is_within_granted_area = token.capability.granted_area().includes_entry(&entry); + let is_write_cap = token.capability.access_mode() == &AccessMode::Write; + + let mut consumer = IntoVec::::new(); + entry.encode(&mut consumer).unwrap(); + let message = consumer.into_vec(); + + let expected_sig = token + .capability + .receiver() + .corresponding_secret_key() + .sign(&message); + + if token.is_authorised_write(&entry) { + assert!(is_write_cap); + assert!(is_within_granted_area); + assert_eq!(token.signature, expected_sig); + } else { + assert!(!is_write_cap || !is_within_granted_area || token.signature != expected_sig); + } +}); diff --git a/fuzz/src/placeholder_params.rs b/fuzz/src/placeholder_params.rs index 979e369..212eef6 100644 --- a/fuzz/src/placeholder_params.rs +++ b/fuzz/src/placeholder_params.rs @@ -1,9 +1,11 @@ use arbitrary::Arbitrary; use ufotofu::local_nb::{BulkConsumer, BulkProducer}; +use ufotofu::sync::{BulkConsumer as BulkConsumerSync, BulkProducer as BulkProducerSync}; use willow_data_model::{ encoding::{ error::DecodeError, parameters::{Decodable, Encodable}, + parameters_sync::{Decodable as DecodableSync, Encodable as EncodableSync}, }, parameters::PayloadDigest, }; @@ -38,4 +40,30 @@ impl Decodable for FakePayloadDigest { } } +impl EncodableSync for FakePayloadDigest { + fn encode(&self, consumer: &mut C) -> Result<(), C::Error> + where + C: BulkConsumerSync, + { + consumer + .bulk_consume_full_slice(&self.0) + .map_err(|f| f.reason)?; + + Ok(()) + } +} + +impl DecodableSync for FakePayloadDigest { + fn decode

(producer: &mut P) -> Result> + where + P: BulkProducerSync, + { + let mut slice = [0u8; 32]; + + producer.bulk_overwrite_full_slice(&mut slice)?; + + Ok(FakePayloadDigest(slice)) + } +} + impl PayloadDigest for FakePayloadDigest {} diff --git a/meadowcap/Cargo.toml b/meadowcap/Cargo.toml index 28692bd..ff840f7 100644 --- a/meadowcap/Cargo.toml +++ b/meadowcap/Cargo.toml @@ -11,6 +11,7 @@ dev = ["dep:arbitrary"] signature = "2.2.0" ufotofu = "0.3.0" arbitrary = { version = "1.0.2", features = ["derive"], optional = true } +either = "1.13.0" [dependencies.willow-data-model] path = "../data-model" diff --git a/meadowcap/src/mc_authorisation_token.rs b/meadowcap/src/mc_authorisation_token.rs index 39ac185..cb4e3dc 100644 --- a/meadowcap/src/mc_authorisation_token.rs +++ b/meadowcap/src/mc_authorisation_token.rs @@ -5,11 +5,12 @@ use willow_data_model::{ parameters::{IsAuthorisedWrite, NamespaceId, PayloadDigest, SubspaceId}, }; -use crate::{mc_capability::McCapability, IsCommunal}; +use crate::{mc_capability::McCapability, AccessMode, IsCommunal}; /// To be used as an AuthorisationToken for Willow. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#MeadowcapAuthorisationToken) +#[derive(Debug, Clone)] pub struct McAuthorisationToken< const MCL: usize, const MCC: usize, @@ -75,6 +76,15 @@ where PD, >, ) -> bool { + match self.capability.access_mode() { + AccessMode::Read => return false, + AccessMode::Write => {} + } + + if !self.capability.granted_area().includes_entry(entry) { + return false; + } + let mut consumer = IntoVec::::new(); entry.encode(&mut consumer).unwrap(); @@ -90,3 +100,53 @@ where true } } + +#[cfg(feature = "dev")] +use arbitrary::Arbitrary; + +#[cfg(feature = "dev")] +impl< + 'a, + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > Arbitrary<'a> + for McAuthorisationToken< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > +where + NamespacePublicKey: + NamespaceId + Encodable + IsCommunal + Arbitrary<'a> + Verifier, + UserPublicKey: SubspaceId + Encodable + Verifier + Arbitrary<'a>, + NamespaceSignature: Encodable + Clone + Arbitrary<'a>, + UserSignature: Encodable + Clone + Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let capability: McCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > = Arbitrary::arbitrary(u)?; + + let signature: UserSignature = Arbitrary::arbitrary(u)?; + + Ok(Self { + capability, + signature, + }) + } +} diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index dbf960a..a712bd9 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -1,3 +1,4 @@ +use either::Either; use signature::{Error as SignatureError, Signer, Verifier}; use ufotofu::sync::consumer::IntoVec; use willow_data_model::{ @@ -14,6 +15,31 @@ use crate::{ AccessMode, Delegation, FailedDelegationError, InvalidDelegationError, IsCommunal, }; +pub struct NotAWriteCapabilityError< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, +>( + McCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + >, +) +where + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, + UserPublicKey: SubspaceId + Encodable + Verifier, + NamespaceSignature: Encodable + Clone, + UserSignature: Encodable + Clone; + /// A Meadowcap capability. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#Capability) @@ -178,27 +204,43 @@ where &self, entry: Entry, secret: UserSecret, - ) -> McAuthorisationToken< - MCL, - MCC, - MPL, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, + ) -> Result< + McAuthorisationToken< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + >, + NotAWriteCapabilityError< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + >, > where UserSecret: Signer, PD: PayloadDigest + Encodable, { - let mut consumer = IntoVec::::new(); - entry.encode(&mut consumer).unwrap(); + match self.access_mode() { + AccessMode::Read => Err(NotAWriteCapabilityError(self.clone())), + AccessMode::Write => { + let mut consumer = IntoVec::::new(); + entry.encode(&mut consumer).unwrap(); - let signature = secret.sign(&consumer.into_vec()); + let signature = secret.sign(&consumer.into_vec()); - McAuthorisationToken { - capability: self.clone(), - signature, + Ok(McAuthorisationToken { + capability: self.clone(), + signature, + }) + } } } @@ -217,25 +259,43 @@ where UserPublicKey, UserSignature, >, - SignatureError, + Either< + NotAWriteCapabilityError< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + >, + SignatureError, + >, > where UserSecret: Signer, PD: PayloadDigest + Encodable, { - let mut consumer = IntoVec::::new(); - entry.encode(&mut consumer).unwrap(); + match self.access_mode() { + AccessMode::Read => Err(Either::Left(NotAWriteCapabilityError(self.clone()))), + AccessMode::Write => { + let mut consumer = IntoVec::::new(); + entry.encode(&mut consumer).unwrap(); - let message = consumer.into_vec(); + let message = consumer.into_vec(); - let signature = secret.sign(&message); + let signature = secret.sign(&message); - self.receiver().verify(&message, &signature)?; + self.receiver() + .verify(&message, &signature) + .map_err(|err| Either::Right(err))?; - Ok(McAuthorisationToken { - capability: self.clone(), - signature, - }) + Ok(McAuthorisationToken { + capability: self.clone(), + signature, + }) + } + } } } From a4a4ba11f5d9e814ecc7d24bdf22cc95690b981d Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Fri, 2 Aug 2024 16:22:53 +0100 Subject: [PATCH 19/27] McCapability encoding --- Cargo.lock | 2 + data-model/src/encoding/bytes.rs | 2 +- data-model/src/encoding/compact_width.rs | 2 +- data-model/src/encoding/mod.rs | 3 +- fuzz/Cargo.toml | 15 ++ .../mc_capability_rel_area_encoding.rs | 49 ++++ .../mc_capability_rel_area_encoding_random.rs | 19 ++ fuzz/src/encode.rs | 2 +- fuzz/src/silly_sigs.rs | 73 ++++-- meadowcap/Cargo.toml | 1 + meadowcap/src/communal_capability.rs | 6 +- meadowcap/src/lib.rs | 1 + meadowcap/src/mc_capability.rs | 236 ++++++++++++++++++ meadowcap/src/owned_capability.rs | 43 ++++ 14 files changed, 433 insertions(+), 21 deletions(-) create mode 100644 fuzz/fuzz_targets/mc_capability_rel_area_encoding.rs create mode 100644 fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs diff --git a/Cargo.lock b/Cargo.lock index f49c64f..780f07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,6 +341,7 @@ dependencies = [ "arbitrary", "either", "signature", + "syncify", "ufotofu", "willow-data-model", ] @@ -578,6 +579,7 @@ dependencies = [ "meadowcap", "signature", "smol", + "syncify", "ufotofu", "willow-data-model", ] diff --git a/data-model/src/encoding/bytes.rs b/data-model/src/encoding/bytes.rs index 7af61b1..40f8609 100644 --- a/data-model/src/encoding/bytes.rs +++ b/data-model/src/encoding/bytes.rs @@ -1,7 +1,7 @@ use syncify::syncify; #[syncify(encoding_sync)] -pub(super) mod encoding { +pub mod encoding { use crate::encoding::error::DecodeError; diff --git a/data-model/src/encoding/compact_width.rs b/data-model/src/encoding/compact_width.rs index fffc7f9..2018bca 100644 --- a/data-model/src/encoding/compact_width.rs +++ b/data-model/src/encoding/compact_width.rs @@ -109,7 +109,7 @@ use syncify::syncify; use syncify::syncify_replace; #[syncify(encoding_sync)] -pub(super) mod encoding { +pub mod encoding { use super::*; #[syncify_replace(use ufotofu::sync::{BulkConsumer, BulkProducer};)] diff --git a/data-model/src/encoding/mod.rs b/data-model/src/encoding/mod.rs index b43f4d8..2b90352 100644 --- a/data-model/src/encoding/mod.rs +++ b/data-model/src/encoding/mod.rs @@ -10,7 +10,6 @@ pub mod unsigned_int; pub mod max_power; pub mod max_power_sync { use super::max_power; - pub use max_power::encoding_sync::*; pub use max_power::max_power; -} \ No newline at end of file +} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index f2b8c1d..c2c4466 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -16,6 +16,7 @@ smol = "2.0.0" arbitrary = { version = "1.0.2", features = ["derive"] } libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } signature = "2.2.0" +syncify = "0.1.0" [dependencies.willow-data-model] @@ -344,3 +345,17 @@ path = "fuzz_targets/mc_is_authorised_write.rs" test = false doc = false bench = false + +[[bin]] +name = "mc_capability_rel_area_encoding" +path = "fuzz_targets/mc_capability_rel_area_encoding.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "mc_capability_rel_area_encoding_random" +path = "fuzz_targets/mc_capability_rel_area_encoding_random.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/mc_capability_rel_area_encoding.rs b/fuzz/fuzz_targets/mc_capability_rel_area_encoding.rs new file mode 100644 index 0000000..e4cc38b --- /dev/null +++ b/fuzz/fuzz_targets/mc_capability_rel_area_encoding.rs @@ -0,0 +1,49 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::mc_capability::McCapability; +use ufotofu::local_nb::consumer::TestConsumer; +use willow_data_model::grouping::area::Area; +use willow_data_model_fuzz::encode::relative_encoding_roundtrip; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; + +fuzz_target!(|data: ( + McCapability<3, 3, 3, SillyPublicKey, SillySig, SillyPublicKey, SillySig>, + Area<3, 3, 3, SillyPublicKey>, + Vec, + TestConsumer +)| { + let (mc_cap, out, delegees, mut consumer) = data; + + if !out.includes_area(&mc_cap.granted_area()) { + return; + } + + let mut cap_with_delegees = mc_cap.clone(); + let mut last_receiver = mc_cap.receiver().clone(); + let granted_area = mc_cap.granted_area(); + + for delegee in delegees { + cap_with_delegees = cap_with_delegees + .delegate( + &last_receiver.corresponding_secret_key(), + &delegee, + &granted_area, + ) + .unwrap(); + last_receiver = delegee; + } + + if !out.includes_area(&cap_with_delegees.granted_area()) { + return; + } + + smol::block_on(async { + relative_encoding_roundtrip::< + McCapability<3, 3, 3, SillyPublicKey, SillySig, SillyPublicKey, SillySig>, + Area<3, 3, 3, SillyPublicKey>, + TestConsumer, + >(cap_with_delegees, out, &mut consumer) + .await; + }); +}); diff --git a/fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs b/fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs new file mode 100644 index 0000000..be09447 --- /dev/null +++ b/fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs @@ -0,0 +1,19 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::mc_capability::McCapability; +use willow_data_model::grouping::area::Area; +use willow_data_model_fuzz::encode::relative_encoding_random_less_strict; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; + +fuzz_target!(|data: (&[u8], Area<3, 3, 3, SillyPublicKey>,)| { + let (random_bytes, out) = data; + + smol::block_on(async { + relative_encoding_random_less_strict::< + Area<3, 3, 3, SillyPublicKey>, + McCapability<3, 3, 3, SillyPublicKey, SillySig, SillyPublicKey, SillySig>, + >(out, random_bytes) + .await; + }); +}); diff --git a/fuzz/src/encode.rs b/fuzz/src/encode.rs index 6210c25..ad14213 100644 --- a/fuzz/src/encode.rs +++ b/fuzz/src/encode.rs @@ -79,7 +79,7 @@ pub async fn relative_encoding_roundtrip( C: BulkConsumer, { //println!("item {:?}", subject); - // println!("ref {:?}", reference); + //println!("ref {:?}", reference); if let Err(_err) = subject.relative_encode(&reference, consumer).await { return; diff --git a/fuzz/src/silly_sigs.rs b/fuzz/src/silly_sigs.rs index e0a0865..5f22cec 100644 --- a/fuzz/src/silly_sigs.rs +++ b/fuzz/src/silly_sigs.rs @@ -77,24 +77,67 @@ impl IsCommunal for SillyPublicKey { } } -impl Encodable for SillyPublicKey { - fn encode(&self, consumer: &mut Consumer) -> Result<(), Consumer::Error> - where - Consumer: ufotofu::sync::BulkConsumer, - { - consumer.consume(self.0)?; +use syncify::{syncify, syncify_replace}; - Ok(()) +#[syncify(encoding_sync)] +pub mod encoding { + use super::*; + + #[syncify_replace(use ufotofu::sync::{BulkConsumer, BulkProducer};)] + use ufotofu::local_nb::{BulkConsumer, BulkProducer}; + + use willow_data_model::encoding::error::DecodeError; + #[syncify_replace(use willow_data_model::encoding::parameters_sync::{Encodable, Decodable};)] + use willow_data_model::encoding::parameters::{Decodable, Encodable}; + + #[syncify_replace(use willow_data_model::encoding::bytes::encoding_sync::produce_byte;)] + use willow_data_model::encoding::bytes::encoding::produce_byte; + + impl Encodable for SillyPublicKey { + async fn encode(&self, consumer: &mut Consumer) -> Result<(), Consumer::Error> + where + Consumer: BulkConsumer, + { + consumer.consume(self.0).await?; + + Ok(()) + } } -} -impl Encodable for SillySig { - fn encode(&self, consumer: &mut Consumer) -> Result<(), Consumer::Error> - where - Consumer: ufotofu::sync::BulkConsumer, - { - consumer.consume(self.0)?; + impl Decodable for SillyPublicKey { + async fn decode( + producer: &mut Producer, + ) -> Result> + where + Producer: BulkProducer, + { + let num = produce_byte(producer).await?; - Ok(()) + Ok(SillyPublicKey(num)) + } + } + + impl Encodable for SillySig { + async fn encode(&self, consumer: &mut Consumer) -> Result<(), Consumer::Error> + where + Consumer: BulkConsumer, + { + consumer.consume(self.0).await?; + + Ok(()) + } + } + + impl Decodable for SillySig { + async fn decode( + producer: &mut Producer, + ) -> Result> + where + Producer: BulkProducer, + { + let num = produce_byte(producer).await?; + + Ok(SillySig(num)) + } } } diff --git a/meadowcap/Cargo.toml b/meadowcap/Cargo.toml index ff840f7..ab84529 100644 --- a/meadowcap/Cargo.toml +++ b/meadowcap/Cargo.toml @@ -12,6 +12,7 @@ signature = "2.2.0" ufotofu = "0.3.0" arbitrary = { version = "1.0.2", features = ["derive"], optional = true } either = "1.13.0" +syncify = "0.1.0" [dependencies.willow-data-model] path = "../data-model" diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs index 29379ca..d92419d 100644 --- a/meadowcap/src/communal_capability.rs +++ b/meadowcap/src/communal_capability.rs @@ -190,10 +190,14 @@ where &self.delegations } + /// Return the public key of the very first user this capability was issued to. + pub fn progenitor(&self) -> &UserPublicKey { + &self.user_key + } + /// A bytestring to be signed for a new [`Delegation`]. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#communal_handover) - fn handover( &self, new_area: &Area, diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 47055a9..6dcf011 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -115,6 +115,7 @@ pub enum FailedDelegationError< } /// Returned when an existing delegation was found to be invalid. +#[derive(Debug)] pub enum InvalidDelegationError< const MCL: usize, const MCC: usize, diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index a712bd9..a5cb7cb 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -161,6 +161,14 @@ where } } + /// Return the public key of the very first user this capability was issued to. + pub fn progenitor(&self) -> &UserPublicKey { + match self { + McCapability::Communal(cap) => cap.progenitor(), + McCapability::Owned(cap) => cap.progenitor(), + } + } + /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. /// Will fail if the area is not included by this capability's granted area, or if the given secret key does not correspond to the capability's receiver. pub fn delegate( @@ -299,6 +307,234 @@ where } } +use syncify::syncify; +use syncify::syncify_replace; + +#[syncify(encoding_sync)] +pub(super) mod encoding { + use super::*; + + #[syncify_replace(use ufotofu::sync::{BulkConsumer, BulkProducer};)] + use ufotofu::local_nb::{BulkConsumer, BulkProducer}; + + #[syncify_replace(use willow_data_model::encoding::parameters_sync::{Encodable, Decodable, RelativeDecodable, RelativeEncodable};)] + use willow_data_model::encoding::parameters::{ + Decodable, Encodable, RelativeDecodable, RelativeEncodable, + }; + + use willow_data_model::encoding::{ + bytes::is_bitflagged, compact_width::CompactWidth, error::DecodeError, + parameters_sync::Encodable as EncodableSync, + }; + + #[syncify_replace(use willow_data_model::encoding::bytes::encoding_sync::produce_byte;)] + use willow_data_model::encoding::bytes::encoding::produce_byte; + + #[syncify_replace( + use willow_data_model::encoding::compact_width::encoding_sync::{encode_compact_width_be, decode_compact_width_be}; + )] + use willow_data_model::encoding::compact_width::encoding::{ + decode_compact_width_be, encode_compact_width_be, + }; + + impl< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > RelativeEncodable> + for McCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > + where + NamespacePublicKey: + NamespaceId + EncodableSync + Encodable + Verifier + IsCommunal, + UserPublicKey: SubspaceId + EncodableSync + Encodable + Verifier, + NamespaceSignature: EncodableSync + Encodable + Clone, + UserSignature: EncodableSync + Encodable + Clone, + { + async fn relative_encode( + &self, + out: &Area, + consumer: &mut Consumer, + ) -> Result<(), Consumer::Error> + where + Consumer: BulkConsumer, + { + let mut header: u8 = 0; + + match self { + McCapability::Communal(cap) => { + if self.access_mode() == &AccessMode::Write { + header |= 0b0100_0000; + } + } + McCapability::Owned(cap) => { + if self.access_mode() == &AccessMode::Read { + header |= 0b1000_0000; + } else { + header |= 0b1100_0000; + } + } + } + + let delegations_count = self.delegations().len(); + + if delegations_count >= 4294967296 { + header |= 0b0011_1111; + } else if delegations_count >= 65536 { + header |= 0b0011_1110; + } else if delegations_count >= 256 { + header |= 0b0011_1101; + } else if delegations_count >= 60 { + header |= 0b0011_1100; + } else { + header |= delegations_count as u8; + } + + consumer.consume(header).await?; + + Encodable::encode(self.granted_namespace(), consumer).await?; + Encodable::encode(self.progenitor(), consumer).await?; + + match self { + McCapability::Communal(_) => {} + McCapability::Owned(cap) => { + Encodable::encode(cap.initial_authorisation(), consumer).await?; + } + }; + + if delegations_count >= 60 { + encode_compact_width_be(delegations_count as u64, consumer).await?; + } + + let mut prev_area = out.clone(); + + for delegation in self.delegations().iter() { + delegation + .area + .relative_encode(&prev_area, consumer) + .await?; + // TODO: Not clone the delegation every time, learn how to make this lifetime live long enough properly from Aljoscha + prev_area = delegation.area.clone(); + Encodable::encode(&delegation.user, consumer).await?; + Encodable::encode(&delegation.signature, consumer).await?; + } + + Ok(()) + } + } + + impl< + const MCL: usize, + const MCC: usize, + const MPL: usize, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > RelativeDecodable> + for McCapability< + MCL, + MCC, + MPL, + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > + where + NamespacePublicKey: + NamespaceId + EncodableSync + Decodable + Verifier + IsCommunal, + UserPublicKey: SubspaceId + EncodableSync + Decodable + Verifier, + NamespaceSignature: EncodableSync + Decodable + Clone, + UserSignature: EncodableSync + Decodable + Clone, + { + async fn relative_decode( + out: &Area, + producer: &mut Producer, + ) -> Result> + where + Producer: BulkProducer, + Self: Sized, + { + let header = produce_byte(producer).await?; + + let is_owned = is_bitflagged(header, 0); + let access_mode = if is_bitflagged(header, 1) { + AccessMode::Write + } else { + AccessMode::Read + }; + + let namespace_key = NamespacePublicKey::decode(producer).await?; + let user_key = UserPublicKey::decode(producer).await?; + + let mut base_cap = if is_owned { + let initial_authorisation = NamespaceSignature::decode(producer).await?; + + let cap = OwnedCapability::from_existing( + namespace_key, + user_key, + initial_authorisation, + access_mode, + ) + .map_err(|_| DecodeError::InvalidInput)?; + + Self::Owned(cap) + } else { + let cap = CommunalCapability::new(namespace_key, user_key, access_mode) + .map_err(|_| DecodeError::InvalidInput)?; + + Self::Communal(cap) + }; + + let delegations_to_decode = if header & 0b0011_1111 == 0b0011_1111 { + decode_compact_width_be(CompactWidth::Eight, producer).await? + } else if header & 0b0011_1110 == 0b0011_1110 { + decode_compact_width_be(CompactWidth::Four, producer).await? + } else if header & 0b0011_1101 == 0b0011_1101 { + decode_compact_width_be(CompactWidth::Two, producer).await? + } else if header & 0b0011_1100 == 0b0011_1100 { + decode_compact_width_be(CompactWidth::One, producer).await? + } else { + (header & 0b0011_1111) as u64 + }; + + let mut prev_area = out.clone(); + + for _ in 0..delegations_to_decode { + let area = + Area::::relative_decode(&prev_area, producer) + .await?; + // Yeah this doesn't feel good. + prev_area = area.clone(); + let user = UserPublicKey::decode(producer).await?; + let signature = UserSignature::decode(producer).await?; + + base_cap + .append_existing_delegation(Delegation { + area, + user, + signature, + }) + .map_err(|_| DecodeError::InvalidInput)?; + } + + Ok(base_cap) + } + } +} + #[cfg(feature = "dev")] use arbitrary::Arbitrary; diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index 0181e82..8e7f1ca 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -108,6 +108,39 @@ where }) } + /// Instantiate an [`OwnedCapability`] using an existing authorisation (e.g. one received over the network), or return an error if the signature was not created by the namespace key. + pub fn from_existing( + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + initial_authorisation: NamespaceSignature, + access_mode: AccessMode, + ) -> Result> { + let mut consumer = IntoVec::::new(); + + let access_mode_byte = match access_mode { + AccessMode::Read => 0x02, + AccessMode::Write => 0x03, + }; + + // We can safely unwrap as IntoVec's error type is ! (never). + consumer.consume(access_mode_byte).unwrap(); + user_key.encode(&mut consumer).unwrap(); + + let message = consumer.into_vec(); + + namespace_key + .verify(&message, &initial_authorisation) + .map_err(|err| OwnedCapabilityCreationError::InvalidSignature(err))?; + + Ok(Self { + access_mode, + namespace_key, + user_key, + initial_authorisation, + delegations: Vec::new(), + }) + } + /// Delegate this capability to a new [`UserPublicKey`] for a given [`Area`]. /// Will fail if the area is not included by this capability's [granted area](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_granted_area), or if the given secret key does not correspond to the capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). pub fn delegate( @@ -235,6 +268,16 @@ where &self.delegations } + /// Return the public key of the very first user this capability was issued to. + pub fn progenitor(&self) -> &UserPublicKey { + &self.user_key + } + + /// Return the original signature authorising this namespace capability. + pub fn initial_authorisation(&self) -> &NamespaceSignature { + &self.initial_authorisation + } + /// A bytestring to be signed for a new [`Delegation`]. /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_handover) From 038ef2516a70dde98dd3c77d77faef69f7a9f388 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Sat, 3 Aug 2024 15:17:57 +0100 Subject: [PATCH 20/27] Add McSubspaceCapability + fuzz tests --- fuzz/Cargo.toml | 28 ++ ...c_subspace_capability_append_delegation.rs | 61 +++ .../mc_subspace_capability_delegation.rs | 38 ++ .../mc_subspace_capability_encoding.rs | 33 ++ .../mc_subspace_capability_encoding_random.rs | 15 + fuzz/src/encode.rs | 47 ++- fuzz/src/silly_sigs.rs | 5 +- meadowcap/src/lib.rs | 1 + meadowcap/src/mc_capability.rs | 6 +- meadowcap/src/owned_capability.rs | 22 +- meadowcap/src/subspace_capability.rs | 365 ++++++++++++++++++ 11 files changed, 611 insertions(+), 10 deletions(-) create mode 100644 fuzz/fuzz_targets/mc_subspace_capability_append_delegation.rs create mode 100644 fuzz/fuzz_targets/mc_subspace_capability_delegation.rs create mode 100644 fuzz/fuzz_targets/mc_subspace_capability_encoding.rs create mode 100644 fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs create mode 100644 meadowcap/src/subspace_capability.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c2c4466..e7f7222 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -359,3 +359,31 @@ path = "fuzz_targets/mc_capability_rel_area_encoding_random.rs" test = false doc = false bench = false + +[[bin]] +name = "mc_subspace_capability_delegation" +path = "fuzz_targets/mc_subspace_capability_delegation.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "mc_subspace_capability_append_delegation" +path = "fuzz_targets/mc_subspace_capability_append_delegation.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "mc_subspace_capability_encoding" +path = "fuzz_targets/mc_subspace_capability_encoding.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "mc_subspace_capability_encoding_random" +path = "fuzz_targets/mc_subspace_capability_encoding_random.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/mc_subspace_capability_append_delegation.rs b/fuzz/fuzz_targets/mc_subspace_capability_append_delegation.rs new file mode 100644 index 0000000..5fff192 --- /dev/null +++ b/fuzz/fuzz_targets/mc_subspace_capability_append_delegation.rs @@ -0,0 +1,61 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::subspace_capability::McSubspaceCapability; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; + +fuzz_target!(|data: ( + (SillyPublicKey, SillySig), + McSubspaceCapability, + Vec +)| { + let (delegation, mc_cap, delegees) = data; + + let mut mut_cap = mc_cap.clone(); + + let mut last_receiver = mut_cap.receiver().clone(); + + for delegee in delegees { + mut_cap = mut_cap + .delegate(&last_receiver.corresponding_secret_key(), &delegee) + .unwrap(); + last_receiver = delegee; + } + + let (delegation_user, delegation_sig) = delegation.clone(); + + let actual_receiver_secret = mut_cap.receiver().corresponding_secret_key(); + + let cap_before_delegation = mut_cap.clone(); + + match mut_cap.append_existing_delegation(delegation) { + Ok(_) => { + // Because there is only one user who can delegate a given capability, we know what it should look like given the same new_area and new_user. + let expected_cap = + cap_before_delegation.delegate(&actual_receiver_secret, &delegation_user); + + match expected_cap { + Ok(cap) => { + assert_eq!(cap, mut_cap); + } + Err(_) => { + panic!("The delegation should not have been possible") + } + } + } + Err(_) => { + assert_eq!(cap_before_delegation.receiver(), mut_cap.receiver()); + + // Because there is only one user who can delegate a given capability, we know what it should look like given the same new_area and new_user. + let expected_cap = mut_cap.delegate(&actual_receiver_secret, &delegation_user); + + match expected_cap { + Ok(cap) => { + let valid_delegation = cap.delegations().last().unwrap(); + assert!(valid_delegation.1 != delegation_sig); + } + Err(_) => panic!("The expected cap should have been fine..."), + } + } + } +}); diff --git a/fuzz/fuzz_targets/mc_subspace_capability_delegation.rs b/fuzz/fuzz_targets/mc_subspace_capability_delegation.rs new file mode 100644 index 0000000..593e807 --- /dev/null +++ b/fuzz/fuzz_targets/mc_subspace_capability_delegation.rs @@ -0,0 +1,38 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::subspace_capability::McSubspaceCapability; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySecret, SillySig}; + +fuzz_target!(|data: ( + SillySecret, + SillyPublicKey, + McSubspaceCapability, + Vec +)| { + let (secret, new_user, mc_subspace_cap, delegees) = data; + + let mut cap_with_delegees = mc_subspace_cap.clone(); + let mut last_receiver = mc_subspace_cap.receiver().clone(); + + for delegee in delegees { + cap_with_delegees = cap_with_delegees + .delegate(&last_receiver.corresponding_secret_key(), &delegee) + .unwrap(); + last_receiver = delegee; + } + + let is_correct_secret = cap_with_delegees.receiver() == &secret.corresponding_public_key(); + + match cap_with_delegees.delegate(&secret, &new_user) { + Ok(delegated_cap) => { + assert!(is_correct_secret); + + assert_eq!(delegated_cap.receiver(), &new_user); + } + Err(_) => { + assert_eq!(&last_receiver, cap_with_delegees.receiver()); + assert!(secret.corresponding_public_key() != last_receiver); + } + } +}); diff --git a/fuzz/fuzz_targets/mc_subspace_capability_encoding.rs b/fuzz/fuzz_targets/mc_subspace_capability_encoding.rs new file mode 100644 index 0000000..734a67d --- /dev/null +++ b/fuzz/fuzz_targets/mc_subspace_capability_encoding.rs @@ -0,0 +1,33 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::subspace_capability::McSubspaceCapability; +use ufotofu::local_nb::consumer::TestConsumer; +use willow_data_model_fuzz::encode::encoding_roundtrip; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; + +fuzz_target!(|data: ( + McSubspaceCapability, + Vec, + TestConsumer +)| { + let (mc_cap, delegees, mut consumer) = data; + + let mut cap_with_delegees = mc_cap.clone(); + let mut last_receiver = mc_cap.receiver().clone(); + + for delegee in delegees { + cap_with_delegees = cap_with_delegees + .delegate(&last_receiver.corresponding_secret_key(), &delegee) + .unwrap(); + last_receiver = delegee; + } + + smol::block_on(async { + encoding_roundtrip::< + McSubspaceCapability, + TestConsumer, + >(cap_with_delegees, &mut consumer) + .await; + }); +}); diff --git a/fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs b/fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs new file mode 100644 index 0000000..fb43514 --- /dev/null +++ b/fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs @@ -0,0 +1,15 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use meadowcap::subspace_capability::McSubspaceCapability; +use willow_data_model_fuzz::encode::encoding_random_less_strict; +use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; + +fuzz_target!(|data: &[u8]| { + smol::block_on(async { + encoding_random_less_strict::< + McSubspaceCapability, + >(data) + .await; + }); +}); diff --git a/fuzz/src/encode.rs b/fuzz/src/encode.rs index ad14213..43d3072 100644 --- a/fuzz/src/encode.rs +++ b/fuzz/src/encode.rs @@ -40,13 +40,15 @@ where pub async fn encoding_random(data: &[u8]) where - T: Encodable + Decodable, + T: Encodable + Decodable + std::fmt::Debug, { let mut producer = FromSlice::new(data); match T::decode(&mut producer).await { Ok(item) => { - // It decoded to a valid path! Gasp! + // println!("item {:?}", item); + + // It decoded to a valid item! Gasp! // Can we turn it back into the same encoding? let mut consumer = IntoVec::::new(); @@ -69,6 +71,47 @@ where }; } +pub async fn encoding_random_less_strict(data: &[u8]) +where + T: Encodable + Decodable + std::fmt::Debug + PartialEq + Eq, +{ + let mut producer = FromSlice::new(data); + + match T::decode(&mut producer).await { + Ok(item) => { + // println!("item {:?}", item); + + // It decoded to a valid item! Gasp! + // Can we turn it back into the same encoding? + let mut consumer = IntoVec::::new(); + + item.encode(&mut consumer).await.unwrap(); + + let mut producer_2 = FromSlice::new(consumer.as_ref()); + + match T::decode(&mut producer_2).await { + Ok(decoded_again) => { + assert_eq!(item, decoded_again); + } + Err(err) => { + println!("{:?}", err); + panic!("Could not decode again, argh!") + } + } + } + Err(err) => match err { + // There was an error. + DecodeError::Producer(_) => panic!("Returned producer error, when whe shouldn't!"), + DecodeError::InvalidInput => { + // GOOD. + } + DecodeError::U64DoesNotFitUsize => { + panic!("Returned u64DoesNotFitUsize error, when we shouldn't!") + } + }, + }; +} + pub async fn relative_encoding_roundtrip( subject: T, reference: R, diff --git a/fuzz/src/silly_sigs.rs b/fuzz/src/silly_sigs.rs index 5f22cec..0f0354d 100644 --- a/fuzz/src/silly_sigs.rs +++ b/fuzz/src/silly_sigs.rs @@ -1,10 +1,7 @@ use arbitrary::Arbitrary; use meadowcap::IsCommunal; use signature::{Error as SignatureError, Signer, Verifier}; -use willow_data_model::{ - encoding::parameters_sync::Encodable, - parameters::{NamespaceId, SubspaceId}, -}; +use willow_data_model::parameters::{NamespaceId, SubspaceId}; /// A silly, trivial, insecure public key for fuzz testing. #[derive(PartialEq, Eq, Debug, Arbitrary, Clone, Default, PartialOrd, Ord)] diff --git a/meadowcap/src/lib.rs b/meadowcap/src/lib.rs index 6dcf011..ff58021 100644 --- a/meadowcap/src/lib.rs +++ b/meadowcap/src/lib.rs @@ -140,3 +140,4 @@ pub mod communal_capability; pub mod mc_authorisation_token; pub mod mc_capability; pub mod owned_capability; +pub mod subspace_capability; diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index a5cb7cb..49a5807 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -296,7 +296,7 @@ where self.receiver() .verify(&message, &signature) - .map_err(|err| Either::Right(err))?; + .map_err(Either::Right)?; Ok(McAuthorisationToken { capability: self.clone(), @@ -373,12 +373,12 @@ pub(super) mod encoding { let mut header: u8 = 0; match self { - McCapability::Communal(cap) => { + McCapability::Communal(_) => { if self.access_mode() == &AccessMode::Write { header |= 0b0100_0000; } } - McCapability::Owned(cap) => { + McCapability::Owned(_) => { if self.access_mode() == &AccessMode::Read { header |= 0b1000_0000; } else { diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index 8e7f1ca..e3ec238 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -2,8 +2,9 @@ use signature::{Error as SignatureError, Signer, Verifier}; use ufotofu::{sync::consumer::IntoVec, sync::Consumer}; use willow_data_model::{ encoding::parameters_sync::{Encodable, RelativeEncodable}, - grouping::area::Area, + grouping::area::{Area, AreaSubspace}, parameters::{NamespaceId, SubspaceId}, + path::Path, }; use crate::{AccessMode, Delegation, FailedDelegationError, InvalidDelegationError, IsCommunal}; @@ -187,6 +188,25 @@ where }) } + /// Return whether this capability needs a complementing [`McSubspaceCapability`] ((definition))[https://willowprotocol.org/specs/pai/index.html#subspace_capability] to in order to be fully authorised by the Willow General Sync Protocol. + pub fn needs_subspace_cap(&self) -> bool { + if self.access_mode == AccessMode::Write { + return false; + } + + let granted_area = self.granted_area(); + + if granted_area.subspace() != &AreaSubspace::Any { + return false; + } + + if granted_area.path() == &Path::new_empty() { + return false; + } + + true + } + /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. pub fn append_existing_delegation( &mut self, diff --git a/meadowcap/src/subspace_capability.rs b/meadowcap/src/subspace_capability.rs new file mode 100644 index 0000000..3759e38 --- /dev/null +++ b/meadowcap/src/subspace_capability.rs @@ -0,0 +1,365 @@ +use signature::{Error as SignatureError, Signer, Verifier}; +use ufotofu::sync::{consumer::IntoVec, Consumer}; +use willow_data_model::{ + encoding::parameters_sync::Encodable, + parameters::{NamespaceId, SubspaceId}, +}; + +use crate::IsCommunal; + +/// A capability that certifies read access to arbitrary [SubspaceIds](https://willowprotocol.org/specs/data-model/index.html#SubspaceId) at some unspecified non-empty [`willow_data_model::Path`]. +/// +/// [Definition](https://willowprotocol.org/specs/pai/index.html#subspace_capability) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct McSubspaceCapability< + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, +> where + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, + NamespaceSignature: Encodable + Clone, + UserPublicKey: SubspaceId + Encodable + Verifier, + UserSignature: Encodable + Clone, +{ + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + initial_authorisation: NamespaceSignature, + delegations: Vec<(UserPublicKey, UserSignature)>, +} + +impl + McSubspaceCapability +where + NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, + NamespaceSignature: Encodable + Clone, + UserPublicKey: SubspaceId + Encodable + Verifier, + UserSignature: Encodable + Clone, +{ + /// Generate a new [`McSubspaceCapability`] for a given user, or return an error if the given namespace secret is incorrect. + pub fn new( + namespace_key: NamespacePublicKey, + namespace_secret: NamespaceSecret, + user_key: UserPublicKey, + ) -> Result< + McSubspaceCapability, + SignatureError, + > + where + NamespaceSecret: Signer, + { + let mut consumer = IntoVec::::new(); + + // We can unwrap here because IntoVec::Error is ! (never) + consumer.consume(0x2).unwrap(); + user_key.encode(&mut consumer).unwrap(); + let message = consumer.into_vec(); + + let signature = namespace_secret.sign(&message); + + namespace_key.verify(&message, &signature)?; + + Ok(McSubspaceCapability { + namespace_key: namespace_key.clone(), + user_key, + initial_authorisation: signature, + delegations: Vec::new(), + }) + } + + /// Instantiate an [`McSubspaceCapability`] using an existing authorisation (e.g. one received over the network), or return an error if the signature was not created by the namespace key. + pub fn from_existing( + namespace_key: NamespacePublicKey, + user_key: UserPublicKey, + initial_authorisation: NamespaceSignature, + ) -> Result { + let mut consumer = IntoVec::::new(); + + // We can unwrap here because IntoVec::Error is ! (never) + consumer.consume(0x2).unwrap(); + user_key.encode(&mut consumer).unwrap(); + let message = consumer.into_vec(); + + namespace_key.verify(&message, &initial_authorisation)?; + + Ok(Self { + namespace_key, + user_key, + initial_authorisation, + delegations: Vec::new(), + }) + } + + /// The user to whom this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#owned_cap_receiver) + pub fn receiver(&self) -> &UserPublicKey { + if self.delegations.is_empty() { + return &self.user_key; + } + + // We can unwrap here because we know delegations isn't empty. + let last_delegation = self.delegations.last().unwrap(); + &last_delegation.0 + } + + /// The [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) for which this capability grants access. + /// + /// [Definition](https://willowprotocol.org/specs/pai/index.html#subspace_cap_receiver) + pub fn granted_namespace(&self) -> &NamespacePublicKey { + &self.namespace_key + } + + /// Delegate this subspace capability to a new [`UserPublicKey`]. + /// Will fail if the given secret key does not correspond to the subspace capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver). + pub fn delegate( + &self, + secret_key: &UserSecretKey, + new_user: &UserPublicKey, + ) -> Result + where + UserSecretKey: Signer, + { + let prev_user = self.receiver(); + + let handover = self.handover(new_user); + let signature = secret_key.sign(&handover); + + prev_user.verify(&handover, &signature)?; + + let mut new_delegations = self.delegations.clone(); + + new_delegations.push((new_user.clone(), signature)); + + Ok(Self { + namespace_key: self.namespace_key.clone(), + initial_authorisation: self.initial_authorisation.clone(), + user_key: self.user_key.clone(), + delegations: new_delegations, + }) + } + + /// Append an existing delegation to an existing capability, or return an error if the delegation is invalid. + pub fn append_existing_delegation( + &mut self, + delegation: (UserPublicKey, UserSignature), + ) -> Result<(), SignatureError> { + let new_user = &delegation.0; + let new_sig = &delegation.1; + + let handover = self.handover(new_user); + + let prev_receiver = self.receiver(); + + prev_receiver.verify(&handover, new_sig)?; + + self.delegations.push(delegation); + + Ok(()) + } + + /// Return a slice of all [`Delegation`]s made to this capability. + pub fn delegations(&self) -> &[(UserPublicKey, UserSignature)] { + &self.delegations + } + + /// A bytestring to be signed for a new subspace capability delegation. + /// + /// [Definition](https://willowprotocol.org/specs/pai/index.html#subspace_handover) + fn handover(&self, new_user: &UserPublicKey) -> Box<[u8]> { + let mut consumer = IntoVec::::new(); + + if self.delegations.is_empty() { + // We can safely unwrap all these encodings as IntoVec's error is the never type. + + self.initial_authorisation.encode(&mut consumer).unwrap(); + new_user.encode(&mut consumer).unwrap(); + + return consumer.into_vec().into(); + } + + // We can unwrap here because we know that self.delegations is not empty. + let last_delegation = self.delegations.last().unwrap(); + + let prev_signature = &last_delegation.1; + // We can safely unwrap all these encodings as IntoVec's error is the never type. + prev_signature.encode(&mut consumer).unwrap(); + new_user.encode(&mut consumer).unwrap(); + + consumer.into_vec().into() + } +} + +use syncify::syncify; +use syncify::syncify_replace; + +#[syncify(encoding_sync)] +pub(super) mod encoding { + use super::*; + + #[syncify_replace(use ufotofu::sync::{BulkConsumer, BulkProducer};)] + use ufotofu::local_nb::{BulkConsumer, BulkProducer}; + + #[syncify_replace(use willow_data_model::encoding::parameters_sync::{Encodable, Decodable};)] + use willow_data_model::encoding::parameters::{Decodable, Encodable}; + + use willow_data_model::encoding::{ + compact_width::CompactWidth, error::DecodeError, + parameters_sync::Encodable as EncodableSync, + }; + + #[syncify_replace(use willow_data_model::encoding::bytes::encoding_sync::produce_byte;)] + use willow_data_model::encoding::bytes::encoding::produce_byte; + + #[syncify_replace( + use willow_data_model::encoding::compact_width::encoding_sync::{encode_compact_width_be, decode_compact_width_be}; + )] + use willow_data_model::encoding::compact_width::encoding::{ + decode_compact_width_be, encode_compact_width_be, + }; + + impl Encodable + for McSubspaceCapability< + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > + where + NamespacePublicKey: + NamespaceId + EncodableSync + Encodable + Verifier + IsCommunal, + NamespaceSignature: EncodableSync + Encodable + Clone, + UserPublicKey: SubspaceId + EncodableSync + Encodable + Verifier, + UserSignature: EncodableSync + Encodable + Clone, + { + async fn encode(&self, consumer: &mut C) -> Result<(), C::Error> + where + C: BulkConsumer, + { + let mut header = 0; + + let delegations_count = self.delegations.len(); + + if delegations_count >= 4294967296 { + header |= 0b1111_1111; + } else if delegations_count >= 65536 { + header |= 0b1111_1110; + } else if delegations_count >= 256 { + header |= 0b1111_1101; + } else if delegations_count >= 60 { + header |= 0b1111_1100; + } else { + header |= delegations_count as u8; + } + + consumer.consume(header).await?; + + Encodable::encode(&self.namespace_key, consumer).await?; + Encodable::encode(&self.user_key, consumer).await?; + Encodable::encode(&self.initial_authorisation, consumer).await?; + + if delegations_count >= 60 { + encode_compact_width_be(delegations_count as u64, consumer).await?; + } + + for delegation in self.delegations.iter() { + Encodable::encode(&delegation.0, consumer).await?; + Encodable::encode(&delegation.1, consumer).await?; + } + + Ok(()) + } + } + + impl Decodable + for McSubspaceCapability< + NamespacePublicKey, + NamespaceSignature, + UserPublicKey, + UserSignature, + > + where + NamespacePublicKey: + NamespaceId + EncodableSync + Decodable + Verifier + IsCommunal, + NamespaceSignature: EncodableSync + Decodable + Clone, + UserPublicKey: SubspaceId + EncodableSync + Decodable + Verifier, + UserSignature: EncodableSync + Decodable + Clone, + { + async fn decode

(producer: &mut P) -> Result> + where + P: BulkProducer, + { + // TODO: Strict encoding relations - this decoder will not throw if the number of delegations claimed is not the same as the number of delegations decoded. + + let header = produce_byte(producer).await?; + + let namespace_key = NamespacePublicKey::decode(producer).await?; + let user_key = UserPublicKey::decode(producer).await?; + let initial_authorisation = NamespaceSignature::decode(producer).await?; + + let mut base_cap = Self::from_existing(namespace_key, user_key, initial_authorisation) + .map_err(|_| DecodeError::InvalidInput)?; + + let delegations_to_decode = if header == 0b1111_1111 { + decode_compact_width_be(CompactWidth::Eight, producer).await? + } else if header == 0b1111_1110 { + decode_compact_width_be(CompactWidth::Four, producer).await? + } else if header == 0b1111_1101 { + decode_compact_width_be(CompactWidth::Two, producer).await? + } else if header == 0b1111_1100 { + decode_compact_width_be(CompactWidth::One, producer).await? + } else { + header as u64 + }; + + for _ in 0..delegations_to_decode { + let user = UserPublicKey::decode(producer).await?; + let signature = UserSignature::decode(producer).await?; + + base_cap + .append_existing_delegation((user, signature)) + .map_err(|_| DecodeError::InvalidInput)?; + } + + Ok(base_cap) + } + } +} + +#[cfg(feature = "dev")] +use arbitrary::{Arbitrary, Error as ArbitraryError}; + +#[cfg(feature = "dev")] +impl<'a, NamespacePublicKey, NamespaceSignature, UserPublicKey, UserSignature> Arbitrary<'a> + for McSubspaceCapability +where + NamespacePublicKey: + NamespaceId + Encodable + IsCommunal + Arbitrary<'a> + Verifier, + UserPublicKey: SubspaceId + Encodable + Verifier + Arbitrary<'a>, + NamespaceSignature: Encodable + Clone + Arbitrary<'a>, + UserSignature: Encodable + Clone, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let namespace_key: NamespacePublicKey = Arbitrary::arbitrary(u)?; + let user_key: UserPublicKey = Arbitrary::arbitrary(u)?; + let initial_authorisation: NamespaceSignature = Arbitrary::arbitrary(u)?; + + let mut consumer = IntoVec::::new(); + + // We can unwrap here because IntoVec::Error is ! (never) + consumer.consume(0x2).unwrap(); + user_key.encode(&mut consumer).unwrap(); + let message = consumer.into_vec(); + + namespace_key + .verify(&message, &initial_authorisation) + .map_err(|_| ArbitraryError::IncorrectFormat)?; + + Ok(Self { + namespace_key, + user_key, + initial_authorisation, + delegations: Vec::new(), + }) + } +} From c3814c7ec0ea6d2c79a9f8eed730a6d5f2beb6ae Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 5 Aug 2024 15:35:21 +0100 Subject: [PATCH 21/27] Derive arbitrary for McCapability --- meadowcap/src/mc_capability.rs | 63 ++-------------------------------- 1 file changed, 3 insertions(+), 60 deletions(-) diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index 49a5807..f5f6bde 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "dev")] +use arbitrary::Arbitrary; use either::Either; use signature::{Error as SignatureError, Signer, Verifier}; use ufotofu::sync::consumer::IntoVec; @@ -44,6 +46,7 @@ where /// /// [Definition](https://willowprotocol.org/specs/meadowcap/index.html#Capability) #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "dev", derive(Arbitrary))] pub enum McCapability< const MCL: usize, const MCC: usize, @@ -534,63 +537,3 @@ pub(super) mod encoding { } } } - -#[cfg(feature = "dev")] -use arbitrary::Arbitrary; - -#[cfg(feature = "dev")] -impl< - 'a, - const MCL: usize, - const MCC: usize, - const MPL: usize, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, - > Arbitrary<'a> - for McCapability< - MCL, - MCC, - MPL, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, - > -where - NamespacePublicKey: - NamespaceId + Encodable + IsCommunal + Arbitrary<'a> + Verifier, - UserPublicKey: SubspaceId + Encodable + Verifier + Arbitrary<'a>, - NamespaceSignature: Encodable + Clone + Arbitrary<'a>, - UserSignature: Encodable + Clone, -{ - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let is_communal: bool = Arbitrary::arbitrary(u)?; - - if is_communal { - let cap: CommunalCapability< - MCL, - MCC, - MPL, - NamespacePublicKey, - UserPublicKey, - UserSignature, - > = Arbitrary::arbitrary(u)?; - - return Ok(Self::Communal(cap)); - } - - let cap: OwnedCapability< - MCL, - MCC, - MPL, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, - > = Arbitrary::arbitrary(u)?; - - Ok(Self::Owned(cap)) - } -} From 9442fae7abfebe3b3177460ac60a7efdac66c64b Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 5 Aug 2024 15:41:54 +0100 Subject: [PATCH 22/27] Add unsafe keyword to McCapability::authorisation_token_unchecked --- meadowcap/src/mc_capability.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index f5f6bde..70b9188 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -211,7 +211,11 @@ where } /// Return a new AuthorisationToken without checking if the resulting signature is correct (e.g. because you are going to immediately do that by constructing an [`willow_data_model::AuthorisedEntry`]). - pub fn authorisation_token( + /// + /// ## Safety + /// + /// This function must be called with this capability's [receiver](https://willowprotocol.org/specs/meadowcap/index.html#communal_cap_receiver)'s corresponding secret key, or a token with an incorrect signature will be produced. + pub unsafe fn authorisation_token_unchecked( &self, entry: Entry, secret: UserSecret, @@ -256,7 +260,7 @@ where } /// Return a new [`AuthorisationToken`], or an error if the resulting signature was not for the capability's receiver. - pub fn authorisation_token_checked( + pub fn authorisation_token( &self, entry: Entry, secret: UserSecret, From eac8b60a333db9110bc50732e5c0abf025d8c862 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 5 Aug 2024 16:44:09 +0100 Subject: [PATCH 23/27] McSubspaceCabiliity::delegations returns Iterator --- meadowcap/src/subspace_capability.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meadowcap/src/subspace_capability.rs b/meadowcap/src/subspace_capability.rs index 3759e38..202d926 100644 --- a/meadowcap/src/subspace_capability.rs +++ b/meadowcap/src/subspace_capability.rs @@ -159,8 +159,8 @@ where } /// Return a slice of all [`Delegation`]s made to this capability. - pub fn delegations(&self) -> &[(UserPublicKey, UserSignature)] { - &self.delegations + pub fn delegations(&self) -> impl Iterator { + self.delegations.iter() } /// A bytestring to be signed for a new subspace capability delegation. From 1d8c7e3105d02582b5bc8d8d8cd07d5a9f484dd7 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 5 Aug 2024 17:08:01 +0100 Subject: [PATCH 24/27] Strict encoding for McCapability --- fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs | 4 ++-- meadowcap/src/mc_capability.rs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs b/fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs index be09447..d923d09 100644 --- a/fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs +++ b/fuzz/fuzz_targets/mc_capability_rel_area_encoding_random.rs @@ -3,14 +3,14 @@ use libfuzzer_sys::fuzz_target; use meadowcap::mc_capability::McCapability; use willow_data_model::grouping::area::Area; -use willow_data_model_fuzz::encode::relative_encoding_random_less_strict; +use willow_data_model_fuzz::encode::relative_encoding_random; use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; fuzz_target!(|data: (&[u8], Area<3, 3, 3, SillyPublicKey>,)| { let (random_bytes, out) = data; smol::block_on(async { - relative_encoding_random_less_strict::< + relative_encoding_random::< Area<3, 3, 3, SillyPublicKey>, McCapability<3, 3, 3, SillyPublicKey, SillySig, SillyPublicKey, SillySig>, >(out, random_bytes) diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index 70b9188..cf6cdd3 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -517,6 +517,11 @@ pub(super) mod encoding { (header & 0b0011_1111) as u64 }; + if header & 0b0011_1100 == 0b0011_1100 && delegations_to_decode < 60 { + // The delegation count should have been encoded directly in the header. + return Err(DecodeError::InvalidInput); + } + let mut prev_area = out.clone(); for _ in 0..delegations_to_decode { From c976bd4575024d210e8e95ffd49d4c8439145262 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 5 Aug 2024 17:33:19 +0100 Subject: [PATCH 25/27] Make subspace cap encoding strict --- .../mc_subspace_capability_encoding_random.rs | 4 ++-- meadowcap/src/subspace_capability.rs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs b/fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs index fb43514..8b3458d 100644 --- a/fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs +++ b/fuzz/fuzz_targets/mc_subspace_capability_encoding_random.rs @@ -2,12 +2,12 @@ use libfuzzer_sys::fuzz_target; use meadowcap::subspace_capability::McSubspaceCapability; -use willow_data_model_fuzz::encode::encoding_random_less_strict; +use willow_data_model_fuzz::encode::encoding_random; use willow_data_model_fuzz::silly_sigs::{SillyPublicKey, SillySig}; fuzz_target!(|data: &[u8]| { smol::block_on(async { - encoding_random_less_strict::< + encoding_random::< McSubspaceCapability, >(data) .await; diff --git a/meadowcap/src/subspace_capability.rs b/meadowcap/src/subspace_capability.rs index 202d926..2b2c635 100644 --- a/meadowcap/src/subspace_capability.rs +++ b/meadowcap/src/subspace_capability.rs @@ -307,9 +307,21 @@ pub(super) mod encoding { } else if header == 0b1111_1101 { decode_compact_width_be(CompactWidth::Two, producer).await? } else if header == 0b1111_1100 { - decode_compact_width_be(CompactWidth::One, producer).await? + let count = decode_compact_width_be(CompactWidth::One, producer).await?; + + if count < 60 { + Err(DecodeError::InvalidInput)?; + } + + count } else { - header as u64 + let count = header as u64; + + if count > 60 { + Err(DecodeError::InvalidInput)?; + } + + count }; for _ in 0..delegations_to_decode { From 538973a03e4289b57fde9600c37a7b426f9608e5 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 5 Aug 2024 18:02:45 +0100 Subject: [PATCH 26/27] Refactor NotAWriteCapabilityError --- meadowcap/src/mc_capability.rs | 54 +++++----------------------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index cf6cdd3..8307d8d 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -17,30 +17,9 @@ use crate::{ AccessMode, Delegation, FailedDelegationError, InvalidDelegationError, IsCommunal, }; -pub struct NotAWriteCapabilityError< - const MCL: usize, - const MCC: usize, - const MPL: usize, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, ->( - McCapability< - MCL, - MCC, - MPL, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, - >, -) -where - NamespacePublicKey: NamespaceId + Encodable + Verifier + IsCommunal, - UserPublicKey: SubspaceId + Encodable + Verifier, - NamespaceSignature: Encodable + Clone, - UserSignature: Encodable + Clone; +/// Returned when an operation which should have been called on a write capability was called on a read capability. +#[derive(Debug)] +pub struct NotAWriteCapabilityError; /// A Meadowcap capability. /// @@ -229,22 +208,14 @@ where UserPublicKey, UserSignature, >, - NotAWriteCapabilityError< - MCL, - MCC, - MPL, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, - >, + NotAWriteCapabilityError, > where UserSecret: Signer, PD: PayloadDigest + Encodable, { match self.access_mode() { - AccessMode::Read => Err(NotAWriteCapabilityError(self.clone())), + AccessMode::Read => Err(NotAWriteCapabilityError), AccessMode::Write => { let mut consumer = IntoVec::::new(); entry.encode(&mut consumer).unwrap(); @@ -274,25 +245,14 @@ where UserPublicKey, UserSignature, >, - Either< - NotAWriteCapabilityError< - MCL, - MCC, - MPL, - NamespacePublicKey, - NamespaceSignature, - UserPublicKey, - UserSignature, - >, - SignatureError, - >, + Either, > where UserSecret: Signer, PD: PayloadDigest + Encodable, { match self.access_mode() { - AccessMode::Read => Err(Either::Left(NotAWriteCapabilityError(self.clone()))), + AccessMode::Read => Err(Either::Left(NotAWriteCapabilityError)), AccessMode::Write => { let mut consumer = IntoVec::::new(); entry.encode(&mut consumer).unwrap(); From 393c5920c4bebf90df8db263360b6646c1385c60 Mon Sep 17 00:00:00 2001 From: Sam Gwilym Date: Mon, 5 Aug 2024 18:47:37 +0100 Subject: [PATCH 27/27] Refactor delegations return type, add delegations_len --- meadowcap/src/communal_capability.rs | 19 +++++++++++++++++-- meadowcap/src/mc_capability.rs | 23 ++++++++++++++++------- meadowcap/src/owned_capability.rs | 19 +++++++++++++++++-- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/meadowcap/src/communal_capability.rs b/meadowcap/src/communal_capability.rs index d92419d..81d903f 100644 --- a/meadowcap/src/communal_capability.rs +++ b/meadowcap/src/communal_capability.rs @@ -185,9 +185,24 @@ where last_delegation.area().clone() } + /// Return a slice of all [`Delegation`]s made to this capability, with a concrete return type. + pub(crate) fn delegations_( + &self, + ) -> core::slice::Iter> { + self.delegations.iter() + } + /// Return a slice of all [`Delegation`]s made to this capability. - pub fn delegations(&self) -> &[Delegation] { - &self.delegations + pub fn delegations( + &self, + ) -> impl ExactSizeIterator> + { + self.delegations_() + } + + /// Return the number of delegations present on this capability. + pub fn delegations_len(&self) -> usize { + self.delegations.len() } /// Return the public key of the very first user this capability was issued to. diff --git a/meadowcap/src/mc_capability.rs b/meadowcap/src/mc_capability.rs index 8307d8d..217c72e 100644 --- a/meadowcap/src/mc_capability.rs +++ b/meadowcap/src/mc_capability.rs @@ -136,10 +136,21 @@ where } /// Return a slice of all [`Delegation`]s made to this capability. - pub fn delegations(&self) -> &[Delegation] { + pub fn delegations( + &self, + ) -> impl ExactSizeIterator> + { + match self { + McCapability::Communal(cap) => cap.delegations_(), + McCapability::Owned(cap) => cap.delegations_(), + } + } + + /// Return the number of delegations present on this capability. + pub fn delegations_len(&self) -> usize { match self { - McCapability::Communal(cap) => cap.delegations(), - McCapability::Owned(cap) => cap.delegations(), + McCapability::Communal(cap) => cap.delegations_len(), + McCapability::Owned(cap) => cap.delegations_len(), } } @@ -354,7 +365,7 @@ pub(super) mod encoding { } } - let delegations_count = self.delegations().len(); + let delegations_count = self.delegations_len(); if delegations_count >= 4294967296 { header |= 0b0011_1111; @@ -386,12 +397,11 @@ pub(super) mod encoding { let mut prev_area = out.clone(); - for delegation in self.delegations().iter() { + for delegation in self.delegations() { delegation .area .relative_encode(&prev_area, consumer) .await?; - // TODO: Not clone the delegation every time, learn how to make this lifetime live long enough properly from Aljoscha prev_area = delegation.area.clone(); Encodable::encode(&delegation.user, consumer).await?; Encodable::encode(&delegation.signature, consumer).await?; @@ -488,7 +498,6 @@ pub(super) mod encoding { let area = Area::::relative_decode(&prev_area, producer) .await?; - // Yeah this doesn't feel good. prev_area = area.clone(); let user = UserPublicKey::decode(producer).await?; let signature = UserSignature::decode(producer).await?; diff --git a/meadowcap/src/owned_capability.rs b/meadowcap/src/owned_capability.rs index e3ec238..1bbcc24 100644 --- a/meadowcap/src/owned_capability.rs +++ b/meadowcap/src/owned_capability.rs @@ -283,9 +283,24 @@ where last_delegation.area().clone() } + /// Return a slice of all [`Delegation`]s made to this capability, with a concrete return type. + pub(crate) fn delegations_( + &self, + ) -> core::slice::Iter> { + self.delegations.iter() + } + /// Return a slice of all [`Delegation`]s made to this capability. - pub fn delegations(&self) -> &[Delegation] { - &self.delegations + pub fn delegations( + &self, + ) -> impl ExactSizeIterator> + { + self.delegations_() + } + + /// Return the number of delegations present on this capability. + pub fn delegations_len(&self) -> usize { + self.delegations.len() } /// Return the public key of the very first user this capability was issued to.