node: Identity document verification

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-09-17 11:54:54 +02:00
parent 071465388c
commit 3e9a4eedae
No known key found for this signature in database
4 changed files with 206 additions and 26 deletions

View File

@ -1,3 +1,4 @@
use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{ffi::OsString, fmt, io, str::FromStr}; use std::{ffi::OsString, fmt, io, str::FromStr};
@ -8,10 +9,10 @@ use radicle_git_ext::Oid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::crypto::{self, Verified}; use crate::crypto::{self, Unverified, Verified};
use crate::hash; use crate::hash;
use crate::serde_ext; use crate::serde_ext;
use crate::storage::Remotes; use crate::storage::{BranchName, Remotes};
pub use crypto::PublicKey; pub use crypto::PublicKey;
@ -162,7 +163,7 @@ pub struct Project {
/// The project identifier. /// The project identifier.
pub id: Id, pub id: Id,
/// The latest project identity document. /// The latest project identity document.
pub doc: Doc, pub doc: Doc<Verified>,
/// The project remotes. /// The project remotes.
pub remotes: Remotes<Verified>, pub remotes: Remotes<Verified>,
/// On-disk file path for this project's repository. /// On-disk file path for this project's repository.
@ -183,17 +184,21 @@ pub struct Delegate {
pub id: Did, pub id: Did,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Doc { #[serde(rename_all = "kebab-case")]
pub struct Doc<V> {
pub name: String, pub name: String,
pub description: String, pub description: String,
pub default_branch: String, pub default_branch: String,
pub version: u32, pub version: u32,
pub parent: Option<Oid>, pub parent: Option<Oid>,
pub delegates: NonEmpty<Delegate>, pub delegates: NonEmpty<Delegate>,
pub threshold: usize,
verified: PhantomData<V>,
} }
impl Doc { impl Doc<Verified> {
pub fn write<W: io::Write>(&self, mut writer: W) -> Result<Id, DocError> { pub fn write<W: io::Write>(&self, mut writer: W) -> Result<Id, DocError> {
let mut buf = Vec::new(); let mut buf = Vec::new();
let mut ser = let mut ser =
@ -211,10 +216,145 @@ impl Doc {
Ok(id) 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<Unverified> {
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<Oid>,
delegates: NonEmpty<Delegate>,
threshold: usize,
) -> Self {
Self {
name,
description,
default_branch,
version: 1,
parent,
delegates,
threshold,
verified: PhantomData,
}
}
pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> { pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes) serde_json::from_slice(bytes)
} }
pub fn verified(self) -> Result<Doc<Verified>, 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)] #[cfg(test)]
@ -224,6 +364,14 @@ mod test {
use quickcheck_macros::quickcheck; use quickcheck_macros::quickcheck;
use std::collections::HashSet; use std::collections::HashSet;
#[quickcheck]
fn prop_encode_decode(doc: Doc<Verified>) {
let mut bytes = Vec::new();
doc.write(&mut bytes).unwrap();
assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc);
}
#[quickcheck] #[quickcheck]
fn prop_key_equality(a: PublicKey, b: PublicKey) { fn prop_key_equality(a: PublicKey, b: PublicKey) {
assert_ne!(a, b); assert_ne!(a, b);

View File

@ -1,7 +1,6 @@
use std::io; use std::io;
use std::path::Path; use std::path::Path;
use nonempty::NonEmpty;
use thiserror::Error; use thiserror::Error;
use crate::crypto::{Signer, Verified}; use crate::crypto::{Signer, Verified};
@ -18,6 +17,8 @@ pub const REMOTE_NAME: &str = "rad";
pub enum InitError { pub enum InitError {
#[error("doc: {0}")] #[error("doc: {0}")]
Doc(#[from] identity::DocError), Doc(#[from] identity::DocError),
#[error("doc: {0}")]
DocVerification(#[from] identity::DocVerificationError),
#[error("git: {0}")] #[error("git: {0}")]
Git(#[from] git2::Error), Git(#[from] git2::Error),
#[error("i/o: {0}")] #[error("i/o: {0}")]
@ -47,14 +48,13 @@ pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
name: String::from("anonymous"), name: String::from("anonymous"),
id: identity::Did::from(*pk), id: identity::Did::from(*pk),
}; };
let doc = identity::Doc { let doc = identity::Doc::initial(
name: name.to_owned(), name.to_owned(),
description: description.to_owned(), description.to_owned(),
default_branch: default_branch.clone(), default_branch.clone(),
version: 1, delegate,
parent: None, )
delegates: NonEmpty::new(delegate), .verified()?;
};
let filename = *identity::IDENTITY_PATH; let filename = *identity::IDENTITY_PATH;
let mut doc_bytes = Vec::new(); let mut doc_bytes = Vec::new();

View File

@ -253,7 +253,10 @@ impl Repository {
Ok(()) Ok(())
} }
pub fn identity(&self, remote: &RemoteId) -> Result<Option<identity::Doc>, refs::Error> { pub fn identity(
&self,
remote: &RemoteId,
) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
let oid = if let Some(oid) = self.reference_oid(remote, &RADICLE_ID_REF)? { let oid = if let Some(oid) = self.reference_oid(remote, &RADICLE_ID_REF)? {
oid oid
} else { } else {
@ -266,6 +269,7 @@ impl Repository {
Ok(doc) => doc, Ok(doc) => doc,
}; };
let doc = identity::Doc::from_json(doc.content()).unwrap(); let doc = identity::Doc::from_json(doc.content()).unwrap();
let doc = doc.verified().unwrap();
Ok(Some(doc)) Ok(Some(doc))
} }

View File

@ -1,5 +1,6 @@
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::hash::Hash; use std::hash::Hash;
use std::iter;
use std::net; use std::net;
use std::ops::RangeBounds; use std::ops::RangeBounds;
use std::path::PathBuf; use std::path::PathBuf;
@ -9,8 +10,8 @@ use nonempty::NonEmpty;
use quickcheck::Arbitrary; use quickcheck::Arbitrary;
use crate::collections::HashMap; use crate::collections::HashMap;
use crate::crypto::{self, Signer, Unverified}; use crate::crypto;
use crate::crypto::{PublicKey, SecretKey}; use crate::crypto::{PublicKey, SecretKey, Signer, Unverified, Verified};
use crate::git; use crate::git;
use crate::hash; use crate::hash;
use crate::identity::{Delegate, Did, Doc, Id, Project}; use crate::identity::{Delegate, Did, Doc, Id, Project};
@ -174,7 +175,7 @@ impl Arbitrary for MockStorage {
impl Arbitrary for Project { impl Arbitrary for Project {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut buf = Vec::new(); let mut buf = Vec::new();
let doc = Doc::arbitrary(g); let doc = Doc::<Verified>::arbitrary(g);
let id = doc.write(&mut buf).unwrap(); let id = doc.write(&mut buf).unwrap();
let remotes = storage::Remotes::arbitrary(g); let remotes = storage::Remotes::arbitrary(g);
let path = PathBuf::arbitrary(g); let path = PathBuf::arbitrary(g);
@ -203,24 +204,51 @@ impl Arbitrary for Delegate {
} }
} }
impl Arbitrary for Doc { impl Arbitrary for Doc<Unverified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let name = String::arbitrary(g); let name = String::arbitrary(g);
let description = String::arbitrary(g); let description = String::arbitrary(g);
let default_branch = String::arbitrary(g); let default_branch = String::arbitrary(g);
let version = u32::arbitrary(g);
let parent = None;
let delegate = Delegate::arbitrary(g); let delegate = Delegate::arbitrary(g);
let delegates = NonEmpty::new(delegate);
Self { Self::initial(name, description, default_branch, delegate)
}
}
impl Arbitrary for Doc<Verified> {
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::<Vec<_>>()
.try_into()
.unwrap();
let threshold = delegates.len() / 2 + 1;
let doc: Doc<Unverified> = Doc::new(
name, name,
description, description,
default_branch, default_branch,
version,
parent, parent,
delegates, delegates,
} threshold,
);
doc.verified().unwrap()
} }
} }