From 3e9a4eedaed767999de09fd51754a132dc7c2e61 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Sat, 17 Sep 2022 11:54:54 +0200 Subject: [PATCH] node: Identity document verification Signed-off-by: Alexis Sellier --- node/src/identity.rs | 160 +++++++++++++++++++++++++++++++++++-- node/src/rad.rs | 18 ++--- node/src/storage/git.rs | 6 +- node/src/test/arbitrary.rs | 48 ++++++++--- 4 files changed, 206 insertions(+), 26 deletions(-) diff --git a/node/src/identity.rs b/node/src/identity.rs index 5c69e983..a1befe34 100644 --- a/node/src/identity.rs +++ b/node/src/identity.rs @@ -1,3 +1,4 @@ +use std::marker::PhantomData; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::{ffi::OsString, fmt, io, str::FromStr}; @@ -8,10 +9,10 @@ use radicle_git_ext::Oid; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::crypto::{self, Verified}; +use crate::crypto::{self, Unverified, Verified}; use crate::hash; use crate::serde_ext; -use crate::storage::Remotes; +use crate::storage::{BranchName, Remotes}; pub use crypto::PublicKey; @@ -162,7 +163,7 @@ pub struct Project { /// The project identifier. pub id: Id, /// The latest project identity document. - pub doc: Doc, + pub doc: Doc, /// The project remotes. pub remotes: Remotes, /// On-disk file path for this project's repository. @@ -183,17 +184,21 @@ pub struct Delegate { pub id: Did, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Doc { +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Doc { pub name: String, pub description: String, pub default_branch: String, pub version: u32, pub parent: Option, pub delegates: NonEmpty, + pub threshold: usize, + + verified: PhantomData, } -impl Doc { +impl Doc { pub fn write(&self, mut writer: W) -> Result { let mut buf = Vec::new(); let mut ser = @@ -211,10 +216,145 @@ impl Doc { Ok(id) } +} + +pub const MAX_STRING_LENGTH: usize = 255; +pub const MAX_DELEGATES: usize = 255; + +#[derive(Error, Debug)] +pub enum DocVerificationError { + #[error("invalid name: {0}")] + Name(&'static str), + #[error("invalid description: {0}")] + Description(&'static str), + #[error("invalid default branch: {0}")] + DefaultBranch(&'static str), + #[error("invalid delegates: {0}")] + Delegates(&'static str), + #[error("invalid version `{0}`")] + Version(u32), + #[error("invalid parent: {0}")] + Parent(&'static str), + #[error("invalid threshold `{0}`: {1}")] + Threshold(usize, &'static str), +} + +impl Doc { + pub fn initial( + name: String, + description: String, + default_branch: BranchName, + delegate: Delegate, + ) -> Self { + Self { + name, + description, + default_branch, + version: 1, + parent: None, + delegates: NonEmpty::new(delegate), + threshold: 1, + verified: PhantomData, + } + } + + pub fn new( + name: String, + description: String, + default_branch: BranchName, + parent: Option, + delegates: NonEmpty, + threshold: usize, + ) -> Self { + Self { + name, + description, + default_branch, + version: 1, + parent, + delegates, + threshold, + verified: PhantomData, + } + } pub fn from_json(bytes: &[u8]) -> Result { serde_json::from_slice(bytes) } + + pub fn verified(self) -> Result, DocVerificationError> { + if self.name.is_empty() { + return Err(DocVerificationError::Name("name cannot be empty")); + } + if self.name.len() > MAX_STRING_LENGTH { + return Err(DocVerificationError::Name("name cannot exceed 255 bytes")); + } + if self.description.len() > MAX_STRING_LENGTH { + return Err(DocVerificationError::Description( + "description cannot exceed 255 bytes", + )); + } + if self.delegates.len() > MAX_DELEGATES { + return Err(DocVerificationError::Delegates( + "number of delegates cannot exceed 255", + )); + } + if self + .delegates + .iter() + .any(|d| d.name.is_empty() || d.name.len() > MAX_STRING_LENGTH) + { + return Err(DocVerificationError::Delegates( + "delegate name must not be empty and must not exceed 255 bytes", + )); + } + if self.delegates.is_empty() { + return Err(DocVerificationError::Delegates( + "delegate list cannot be empty", + )); + } + if self.default_branch.is_empty() { + return Err(DocVerificationError::DefaultBranch( + "default branch cannot be empty", + )); + } + if self.default_branch.len() > MAX_STRING_LENGTH { + return Err(DocVerificationError::DefaultBranch( + "default branch cannot exceed 255 bytes", + )); + } + if let Some(parent) = self.parent { + if parent.is_zero() { + return Err(DocVerificationError::Parent("parent cannot be zero")); + } + } + if self.version != 1 { + return Err(DocVerificationError::Version(self.version)); + } + if self.threshold > self.delegates.len() { + return Err(DocVerificationError::Threshold( + self.threshold, + "threshold cannot exceed number of delegates", + )); + } + if self.threshold == 0 { + return Err(DocVerificationError::Threshold( + self.threshold, + "threshold cannot be zero", + )); + } + + Ok(Doc { + name: self.name, + description: self.description, + delegates: self.delegates, + default_branch: self.default_branch, + parent: self.parent, + version: self.version, + threshold: self.threshold, + verified: PhantomData, + }) + } } #[cfg(test)] @@ -224,6 +364,14 @@ mod test { use quickcheck_macros::quickcheck; use std::collections::HashSet; + #[quickcheck] + fn prop_encode_decode(doc: Doc) { + let mut bytes = Vec::new(); + + doc.write(&mut bytes).unwrap(); + assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc); + } + #[quickcheck] fn prop_key_equality(a: PublicKey, b: PublicKey) { assert_ne!(a, b); diff --git a/node/src/rad.rs b/node/src/rad.rs index 82ea4a41..e615f590 100644 --- a/node/src/rad.rs +++ b/node/src/rad.rs @@ -1,7 +1,6 @@ use std::io; use std::path::Path; -use nonempty::NonEmpty; use thiserror::Error; use crate::crypto::{Signer, Verified}; @@ -18,6 +17,8 @@ pub const REMOTE_NAME: &str = "rad"; pub enum InitError { #[error("doc: {0}")] Doc(#[from] identity::DocError), + #[error("doc: {0}")] + DocVerification(#[from] identity::DocVerificationError), #[error("git: {0}")] Git(#[from] git2::Error), #[error("i/o: {0}")] @@ -47,14 +48,13 @@ pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>( name: String::from("anonymous"), id: identity::Did::from(*pk), }; - let doc = identity::Doc { - name: name.to_owned(), - description: description.to_owned(), - default_branch: default_branch.clone(), - version: 1, - parent: None, - delegates: NonEmpty::new(delegate), - }; + let doc = identity::Doc::initial( + name.to_owned(), + description.to_owned(), + default_branch.clone(), + delegate, + ) + .verified()?; let filename = *identity::IDENTITY_PATH; let mut doc_bytes = Vec::new(); diff --git a/node/src/storage/git.rs b/node/src/storage/git.rs index ed0f296b..76817a5b 100644 --- a/node/src/storage/git.rs +++ b/node/src/storage/git.rs @@ -253,7 +253,10 @@ impl Repository { Ok(()) } - pub fn identity(&self, remote: &RemoteId) -> Result, refs::Error> { + pub fn identity( + &self, + remote: &RemoteId, + ) -> Result>, refs::Error> { let oid = if let Some(oid) = self.reference_oid(remote, &RADICLE_ID_REF)? { oid } else { @@ -266,6 +269,7 @@ impl Repository { Ok(doc) => doc, }; let doc = identity::Doc::from_json(doc.content()).unwrap(); + let doc = doc.verified().unwrap(); Ok(Some(doc)) } diff --git a/node/src/test/arbitrary.rs b/node/src/test/arbitrary.rs index ea61ad90..ae6f8995 100644 --- a/node/src/test/arbitrary.rs +++ b/node/src/test/arbitrary.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, HashSet}; use std::hash::Hash; +use std::iter; use std::net; use std::ops::RangeBounds; use std::path::PathBuf; @@ -9,8 +10,8 @@ use nonempty::NonEmpty; use quickcheck::Arbitrary; use crate::collections::HashMap; -use crate::crypto::{self, Signer, Unverified}; -use crate::crypto::{PublicKey, SecretKey}; +use crate::crypto; +use crate::crypto::{PublicKey, SecretKey, Signer, Unverified, Verified}; use crate::git; use crate::hash; use crate::identity::{Delegate, Did, Doc, Id, Project}; @@ -174,7 +175,7 @@ impl Arbitrary for MockStorage { impl Arbitrary for Project { fn arbitrary(g: &mut quickcheck::Gen) -> Self { let mut buf = Vec::new(); - let doc = Doc::arbitrary(g); + let doc = Doc::::arbitrary(g); let id = doc.write(&mut buf).unwrap(); let remotes = storage::Remotes::arbitrary(g); let path = PathBuf::arbitrary(g); @@ -203,24 +204,51 @@ impl Arbitrary for Delegate { } } -impl Arbitrary for Doc { +impl Arbitrary for Doc { fn arbitrary(g: &mut quickcheck::Gen) -> Self { let name = String::arbitrary(g); let description = String::arbitrary(g); let default_branch = String::arbitrary(g); - let version = u32::arbitrary(g); - let parent = None; let delegate = Delegate::arbitrary(g); - let delegates = NonEmpty::new(delegate); - Self { + Self::initial(name, description, default_branch, delegate) + } +} + +impl Arbitrary for Doc { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let rng = fastrand::Rng::with_seed(u64::arbitrary(g)); + let name = iter::repeat_with(|| rng.alphanumeric()) + .take(rng.usize(1..16)) + .collect(); + let description = iter::repeat_with(|| rng.alphanumeric()) + .take(rng.usize(0..32)) + .collect(); + let default_branch = iter::repeat_with(|| rng.alphanumeric()) + .take(rng.usize(1..16)) + .collect(); + let parent = None; + let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate { + name: iter::repeat_with(|| rng.alphanumeric()) + .take(rng.usize(1..16)) + .collect(), + id: Did::arbitrary(g), + }) + .take(rng.usize(1..6)) + .collect::>() + .try_into() + .unwrap(); + let threshold = delegates.len() / 2 + 1; + let doc: Doc = Doc::new( name, description, default_branch, - version, parent, delegates, - } + threshold, + ); + + doc.verified().unwrap() } }