Refactor identity verification

Introduces a new `DocAt` type.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-12-12 14:33:53 +01:00
parent edafe055b4
commit bdbf111163
No known key found for this signature in database
4 changed files with 69 additions and 57 deletions

View File

@ -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)]

View File

@ -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,

View File

@ -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());
}

View File

@ -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)
}