Refactor identity verification
Introduces a new `DocAt` type. Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
edafe055b4
commit
bdbf111163
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue