Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Identity document verification
Alexis Sellier committed 3 years ago
commit 3e9a4eedaed767999de09fd51754a132dc7c2e61
parent 071465388c83b21778940c861a99c8c3f50f5261
4 files changed +206 -26
modified 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<Verified>,
    /// The project remotes.
    pub remotes: Remotes<Verified>,
    /// 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<V> {
    pub name: String,
    pub description: String,
    pub default_branch: String,
    pub version: u32,
    pub parent: Option<Oid>,
    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> {
        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<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> {
        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)]
@@ -225,6 +365,14 @@ mod test {
    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]
    fn prop_key_equality(a: PublicKey, b: PublicKey) {
        assert_ne!(a, b);

modified 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();
modified node/src/storage/git.rs
@@ -253,7 +253,10 @@ impl Repository {
        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)? {
            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))
    }
modified 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::<Verified>::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<Unverified> {
    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<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,
            description,
            default_branch,
-
            version,
            parent,
            delegates,
-
        }
+
            threshold,
+
        );
+

+
        doc.verified().unwrap()
    }
}