From bdbf1111630116d487317e2a7cfac8123d4730fb Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Mon, 12 Dec 2022 14:33:53 +0100 Subject: [PATCH] Refactor identity verification Introduces a new `DocAt` type. Signed-off-by: Alexis Sellier --- radicle-cli/src/commands/inspect.rs | 3 +- radicle/src/identity.rs | 64 +++++++---------------------- radicle/src/identity/doc.rs | 57 ++++++++++++++++++++++--- radicle/src/storage/git.rs | 2 +- 4 files changed, 69 insertions(+), 57 deletions(-) diff --git a/radicle-cli/src/commands/inspect.rs b/radicle-cli/src/commands/inspect.rs index c9df5880..ced20b8c 100644 --- a/radicle-cli/src/commands/inspect.rs +++ b/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::::blob_at(oid, &repo)?; let content: serde_json::Value = serde_json::from_slice(blob.content())?; let timezone = if tip.time().sign() == '+' { #[allow(deprecated)] diff --git a/radicle/src/identity.rs b/radicle/src/identity.rs index 63027d09..bed0ca55 100644 --- a/radicle/src/identity.rs +++ b/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 { // 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::::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::::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 { )); } - trusted = untrusted; - current = blob.id().into(); + current = untrusted.blob; + trusted = untrusted.doc; + signatures = untrusted.sigs; } Ok(Identity { - root, + root: root.blob, head, current, revision, diff --git a/radicle/src/identity/doc.rs b/radicle/src/identity/doc.rs index ff8d6d57..843699c6 100644 --- a/radicle/src/identity/doc.rs +++ b/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, + /// 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 { + /// The payload section. pub payload: BTreeMap, + /// The delegates section. pub delegates: NonEmpty, + /// The signature threshold. pub threshold: usize, #[serde(skip)] @@ -122,6 +145,11 @@ impl Doc { repo.reference_oid(remote, &git::refs::storage::IDENTITY_BRANCH) .map_err(DocError::from) } + + pub fn blob_at(commit: Oid, repo: &R) -> Result { + repo.blob_at(commit, Path::new(&*PATH)) + .map_err(DocError::from) + } } impl Doc { @@ -164,6 +192,28 @@ impl Doc { Ok((oid, sig)) } + pub fn load_at(oid: Oid, repo: &R) -> Result { + 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 { }) } - pub fn blob_at(commit: Oid, repo: &R) -> Result { - repo.blob_at(commit, Path::new(&*PATH)) - .map_err(DocError::from) - } - pub fn load_at(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::::head(&remote, &repo).unwrap_err(); assert!(err.is_not_found()); - let err = Doc::load_at(oid.into(), &repo).unwrap_err(); + let err = Doc::::load_at(oid.into(), &repo).unwrap_err(); assert!(err.is_not_found()); } diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 94360fcb..94fc5606 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -347,7 +347,7 @@ impl Repository { } } - Doc::load_at(longest.into(), self) + Doc::::load_at(longest.into(), self) .map(|(doc, _)| (longest.into(), doc)) .map_err(ProjectError::from) }