Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor identity verification
Alexis Sellier committed 3 years ago
commit bdbf1111630116d487317e2a7cfac8123d4730fb
parent edafe055b467bda7dbbb56c082e12c93fe7975f6
4 files changed +69 -57
modified radicle-cli/src/commands/inspect.rs
@@ -8,6 +8,7 @@ use anyhow::{anyhow, Context as _};
use chrono::prelude::*;
use json_color::{Color, Colorizer};

+
use radicle::crypto::Unverified;
use radicle::identity::Untrusted;
use radicle::identity::{Doc, Id};
use radicle::storage::{ReadRepository, ReadStorage, WriteStorage};
@@ -154,7 +155,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        for (counter, oid) in history.into_iter().rev().enumerate() {
            let oid = oid?.into();
            let tip = repo.commit(oid)?;
-
            let blob = Doc::blob_at(oid, &repo)?;
+
            let blob = Doc::<Unverified>::blob_at(oid, &repo)?;
            let content: serde_json::Value = serde_json::from_slice(blob.content())?;
            let timezone = if tip.time().sign() == '+' {
                #[allow(deprecated)]
modified radicle/src/identity.rs
@@ -10,7 +10,6 @@ use thiserror::Error;
use crate::crypto;
use crate::crypto::{Signature, Verified};
use crate::git;
-
use crate::storage::git::trailers;
use crate::storage::{ReadRepository, RemoteId};

pub use crypto::PublicKey;
@@ -39,10 +38,6 @@ pub enum IdentityError {
    MissingRootSignatures,
    #[error("commit signature for {0} is invalid: {1}")]
    InvalidSignature(PublicKey, crypto::Error),
-
    #[error("commit message for {0} is invalid")]
-
    InvalidCommitMessage(Oid),
-
    #[error("commit trailers for {0} are invalid: {1}")]
-
    InvalidCommitTrailers(Oid, trailers::Error),
    #[error("threshold not reached: {0} signatures for a threshold of {1}")]
    ThresholdNotReached(usize, usize),
    #[error("identity document error: {0}")]
@@ -103,58 +98,28 @@ impl Identity<Untrusted> {

        // Retrieve root document.
        let root_oid = history.pop().ok_or(IdentityError::MissingRoot)??.into();
-
        let root_blob = Doc::blob_at(root_oid, repo)?;
-
        let root: git::Oid = root_blob.id().into();
-
        let trusted = Doc::from_json(root_blob.content())?;
+
        let root = Doc::<Verified>::load_at(root_oid, repo)?;
        let revision = history.len() as u32;

-
        {
-
            let root_commit = repo.commit(root_oid)?;
-
            let root_msg = root_commit
-
                .message_raw()
-
                .ok_or(IdentityError::InvalidCommitMessage(root_oid))?;
-
            let root_sigs = trailers::parse_signatures(root_msg)
-
                .map_err(|e| IdentityError::InvalidCommitTrailers(root_oid, e))?;
-

-
            for (pk, sig) in &root_sigs {
-
                if let Err(err) = pk.verify(root_blob.content(), sig) {
-
                    return Err(IdentityError::InvalidSignature(*pk, err));
-
                }
-
            }
-
            // Every identity founder must have signed the root document.
-
            for founder in &trusted.delegates {
-
                if !root_sigs.iter().any(|(k, _)| k == &**founder) {
-
                    return Err(IdentityError::MissingRootSignatures);
-
                }
+
        // Every identity founder must have signed the root document.
+
        for founder in &root.doc.delegates {
+
            if !root.sigs.iter().any(|(k, _)| k == &**founder) {
+
                return Err(IdentityError::MissingRootSignatures);
            }
        }

-
        let mut trusted = trusted.verified()?;
-
        let mut current = root;
-
        let mut signatures = Vec::new();
+
        let mut current = root.blob;
+
        let mut trusted = root.doc;
+
        let mut signatures = root.sigs;

        // Traverse the history chronologically.
        for oid in history.into_iter().rev() {
            let oid = oid?;
-
            let blob = Doc::blob_at(oid.into(), repo)?;
-
            let untrusted = Doc::from_json(blob.content()).map_err(doc::DocError::from)?;
-
            let untrusted = untrusted.verified()?;
-
            let commit = repo.commit(oid.into())?;
-
            let msg = commit
-
                .message_raw()
-
                .ok_or_else(|| IdentityError::InvalidCommitMessage(oid.into()))?;
-

-
            // Keys that signed the *current* document version.
-
            signatures = trailers::parse_signatures(msg)
-
                .map_err(|e| IdentityError::InvalidCommitTrailers(oid.into(), e))?;
-
            for (pk, sig) in &signatures {
-
                if let Err(err) = pk.verify(blob.content(), sig) {
-
                    return Err(IdentityError::InvalidSignature(*pk, err));
-
                }
-
            }
+
            let untrusted = Doc::<Verified>::load_at(oid.into(), repo)?;

            // Check that enough delegates signed this next version.
-
            let quorum = signatures
+
            let quorum = untrusted
+
                .sigs
                .iter()
                .filter(|(key, _)| trusted.delegates.iter().any(|d| &**d == key))
                .count();
@@ -165,12 +130,13 @@ impl Identity<Untrusted> {
                ));
            }

-
            trusted = untrusted;
-
            current = blob.id().into();
+
            current = untrusted.blob;
+
            trusted = untrusted.doc;
+
            signatures = untrusted.sigs;
        }

        Ok(Identity {
-
            root,
+
            root: root.blob,
            head,
            current,
            revision,
modified radicle/src/identity/doc.rs
@@ -32,10 +32,16 @@ pub const MAX_DELEGATES: usize = 255;

#[derive(Error, Debug)]
pub enum DocError {
+
    #[error("invalid commit: {0}")]
+
    Commit(&'static str),
    #[error("json: {0}")]
    Json(#[from] serde_json::Error),
    #[error("invalid delegates: {0}")]
    Delegates(&'static str),
+
    #[error("invalid signature for {0}: {1}")]
+
    Signature(PublicKey, crypto::Error),
+
    #[error("invalid commit trailers: {0}")]
+
    Trailers(#[from] trailers::Error),
    #[error("invalid version `{0}`")]
    Version(u32),
    #[error("invalid threshold `{0}`: {1}")]
@@ -106,11 +112,28 @@ impl Deref for Payload {
    }
}

+
/// A verified identity document at a specific commit.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct DocAt {
+
    /// The commit at which this document exists.
+
    pub commit: Oid,
+
    /// The document blob at this commit.
+
    pub blob: Oid,
+
    /// The parsed document.
+
    pub doc: Doc<Verified>,
+
    /// The validated commit signatures.
+
    pub sigs: Vec<(PublicKey, Signature)>,
+
}
+

+
/// An identity document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Doc<V> {
+
    /// The payload section.
    pub payload: BTreeMap<PayloadId, Payload>,
+
    /// The delegates section.
    pub delegates: NonEmpty<Did>,
+
    /// The signature threshold.
    pub threshold: usize,

    #[serde(skip)]
@@ -122,6 +145,11 @@ impl<V> Doc<V> {
        repo.reference_oid(remote, &git::refs::storage::IDENTITY_BRANCH)
            .map_err(DocError::from)
    }
+

+
    pub fn blob_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<git2::Blob, DocError> {
+
        repo.blob_at(commit, Path::new(&*PATH))
+
            .map_err(DocError::from)
+
    }
}

impl Doc<Verified> {
@@ -164,6 +192,28 @@ impl Doc<Verified> {
        Ok((oid, sig))
    }

+
    pub fn load_at<R: ReadRepository>(oid: Oid, repo: &R) -> Result<DocAt, DocError> {
+
        let blob = Self::blob_at(oid, repo)?;
+
        let doc = Doc::from_json(blob.content())?.verified()?;
+
        let commit = repo.commit(oid)?;
+
        let msg = commit
+
            .message_raw()
+
            .ok_or(DocError::Commit("commit message is not UTF-8"))?;
+
        let sigs = trailers::parse_signatures(msg)?;
+

+
        for (pk, sig) in &sigs {
+
            if let Err(err) = pk.verify(blob.content(), sig) {
+
                return Err(DocError::Signature(*pk, err));
+
            }
+
        }
+
        Ok(DocAt {
+
            commit: oid,
+
            doc,
+
            blob: blob.id().into(),
+
            sigs,
+
        })
+
    }
+

    pub fn init(
        doc: &[u8],
        remote: &RemoteId,
@@ -266,11 +316,6 @@ impl Doc<Unverified> {
        })
    }

-
    pub fn blob_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<git2::Blob, DocError> {
-
        repo.blob_at(commit, Path::new(&*PATH))
-
            .map_err(DocError::from)
-
    }
-

    pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<(Self, Oid), DocError> {
        let blob = Self::blob_at(commit, repo)?;
        let doc = Doc::from_json(blob.content())?;
@@ -345,7 +390,7 @@ mod test {
        let err = Doc::<Unverified>::head(&remote, &repo).unwrap_err();
        assert!(err.is_not_found());

-
        let err = Doc::load_at(oid.into(), &repo).unwrap_err();
+
        let err = Doc::<Unverified>::load_at(oid.into(), &repo).unwrap_err();
        assert!(err.is_not_found());
    }

modified radicle/src/storage/git.rs
@@ -347,7 +347,7 @@ impl Repository {
            }
        }

-
        Doc::load_at(longest.into(), self)
+
        Doc::<Unverified>::load_at(longest.into(), self)
            .map(|(doc, _)| (longest.into(), doc))
            .map_err(ProjectError::from)
    }