node: Start on identity history verification

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-09-18 00:01:59 +02:00
parent 73d62de0cc
commit 84119b0265
No known key found for this signature in database
9 changed files with 299 additions and 31 deletions

View File

@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
pub use ed25519::Error;
pub use ed25519::Signature;
/// Verified (used as type witness).
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -48,6 +47,64 @@ where
}
}
/// Cryptographic signature.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct Signature(pub ed25519::Signature);
impl fmt::Display for Signature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let base = multibase::Base::Base58Btc;
write!(f, "{}", multibase::encode(base, &self.to_bytes()))
}
}
#[derive(Error, Debug)]
pub enum SignatureError {
#[error("invalid multibase string: {0}")]
Multibase(#[from] multibase::Error),
#[error("invalid signature: {0}")]
Invalid(#[from] ed25519::Error),
}
impl From<ed25519::Signature> for Signature {
fn from(other: ed25519::Signature) -> Self {
Self(other)
}
}
impl FromStr for Signature {
type Err = SignatureError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (_, bytes) = multibase::decode(s)?;
let sig = ed25519::Signature::try_from(bytes.as_slice())?;
Ok(Self(sig))
}
}
impl Deref for Signature {
type Target = ed25519::Signature;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<[u8; 64]> for Signature {
fn from(bytes: [u8; 64]) -> Self {
Self(ed25519::Signature::from(bytes))
}
}
impl TryFrom<&[u8]> for Signature {
type Error = ed25519::Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
ed25519::Signature::try_from(bytes).map(Self)
}
}
/// The public/verification key.
#[derive(Serialize, Deserialize, Eq, Debug, Copy, Clone)]
#[serde(into = "String", try_from = "String")]

View File

@ -8,6 +8,7 @@ use crate::identity::PublicKey;
use crate::storage::refs::Refs;
use crate::storage::RemoteId;
pub use ext::Error;
pub use ext::Oid;
pub use git_ref_format as fmt;
pub use git_ref_format::{refname, RefStr, RefString};

View File

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::io;
use std::marker::PhantomData;
use std::path::Path;
@ -8,14 +9,26 @@ use radicle_git_ext::Oid;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::crypto::{self, Unverified, Verified};
use crate::crypto;
use crate::crypto::{Signature, Unverified, Verified};
use crate::git;
use crate::hash;
use crate::identity::{Did, Id};
use crate::storage::BranchName;
use crate::storage::git::trailers;
use crate::storage::{BranchName, ReadRepository, RemoteId};
pub use crypto::PublicKey;
/// Untrusted, well-formed input.
#[derive(Clone, Copy, Debug)]
pub struct Untrusted;
/// Signed by quorum of the previous delegation.
#[derive(Clone, Copy, Debug)]
pub struct Trusted;
pub static REFERENCE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("heads/radicle/id"));
pub static PATH: Lazy<&Path> = Lazy::new(|| Path::new("radicle.json"));
pub const MAX_STRING_LENGTH: usize = 255;
pub const MAX_DELEGATES: usize = 255;
@ -25,6 +38,12 @@ pub enum Error {
Json(#[from] serde_json::Error),
#[error("i/o: {0}")]
Io(#[from] io::Error),
#[error("verification: {0}")]
Verification(#[from] VerificationError),
#[error("git: {0}")]
Git(#[from] git::Error),
#[error("git: {0}")]
RawGit(#[from] git2::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -33,13 +52,25 @@ pub struct Delegate {
pub id: Did,
}
impl Delegate {
fn matches(&self, key: &PublicKey) -> bool {
&self.id.0 == key
}
}
impl From<Delegate> for PublicKey {
fn from(delegate: Delegate) -> Self {
delegate.id.0
}
}
#[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 description: String, // TODO: Make optional.
pub default_branch: String, // TODO: Make optional.
pub version: u32, // TODO: Remove this.
pub parent: Option<Oid>,
pub delegates: NonEmpty<Delegate>,
pub threshold: usize,
@ -201,6 +232,132 @@ impl Doc<Unverified> {
verified: PhantomData,
})
}
pub fn blob_at<'r, R: ReadRepository<'r>>(
commit: Oid,
repo: &R,
) -> Result<Option<git2::Blob>, git::Error> {
match repo.blob_at(commit, Path::new(&*PATH)) {
Err(git::ext::Error::NotFound(_)) => Ok(None),
Err(e) => Err(e),
Ok(blob) => Ok(Some(blob)),
}
}
pub fn load_at<'r, R: ReadRepository<'r>>(
commit: Oid,
repo: &R,
) -> Result<Option<(Self, Oid)>, git::Error> {
if let Some(blob) = Self::blob_at(commit, repo)? {
let doc = Doc::from_json(blob.content()).unwrap();
return Ok(Some((doc, blob.id().into())));
}
Ok(None)
}
pub fn load<'r, R: ReadRepository<'r>>(
remote: &RemoteId,
repo: &R,
) -> Result<Option<(Self, Oid)>, git::Error> {
if let Some(oid) = Self::head(remote, repo)? {
Self::load_at(oid, repo)
} else {
Ok(None)
}
}
}
impl<V> Doc<V> {
pub fn head<'r, R: ReadRepository<'r>>(
remote: &RemoteId,
repo: &R,
) -> Result<Option<Oid>, git::Error> {
if let Some(oid) = repo.reference_oid(remote, &REFERENCE_NAME)? {
Ok(Some(oid))
} else {
Ok(None)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Identity<V> {
/// The head of the identity branch. This points to a commit that
/// contains the current document blob.
pub head: Oid,
/// The object id of the initial document blob.
/// This is the canonical identifier for this identity.
pub root: Oid,
/// The object id of the current document blob.
pub current: Oid,
/// The current document.
pub doc: Doc<Verified>,
/// Signatures over this identity.
pub signatures: HashMap<PublicKey, Signature>,
verified: PhantomData<V>,
}
impl Identity<Untrusted> {
pub fn load<'r, R: ReadRepository<'r>>(
_id: &Id,
remote: &RemoteId,
repo: &R,
) -> Result<Option<Self>, Error> {
if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
// Retrieve root document.
let root = history.pop().unwrap()?.into();
let root = Doc::blob_at(root, repo)?.unwrap();
let trusted = Doc::from_json(root.content()).unwrap();
// TODO: Check the root document matches ID.
let mut trusted = trusted.verified()?;
let mut current = root.id().into();
let mut signatures = Vec::new();
for oid in history.into_iter().rev() {
let oid = oid?;
let blob = Doc::blob_at(head, repo)?.unwrap();
let untrusted = Doc::from_json(blob.content()).unwrap();
let untrusted = untrusted.verified()?;
let commit = repo.commit(oid.into())?.unwrap();
let msg = commit.message_raw().unwrap();
// Keys that signed the *current* document version.
signatures = trailers::parse_signatures(msg).unwrap();
for (pk, sig) in &signatures {
if pk.verify(sig, blob.content()).is_err() {
todo!();
}
}
// Check that enough delegates from the previous version signed this version.
let quorum = signatures
.iter()
.filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
.count();
// TODO: Check that difference isn't greater than threshold?
if quorum < trusted.threshold {
todo!();
}
trusted = untrusted;
current = blob.id().into();
}
return Ok(Some(Self {
root: root.id().into(),
head,
current,
doc: trusted,
signatures: signatures.into_iter().collect(),
verified: PhantomData,
}));
}
Ok(None)
}
}
#[cfg(test)]

View File

@ -6,7 +6,6 @@ use thiserror::Error;
use crate::crypto::{Signer, Verified};
use crate::git;
use crate::identity::Id;
use crate::storage::git::RADICLE_ID_REF;
use crate::storage::refs::SignedRefs;
use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
use crate::{identity, storage};
@ -84,7 +83,7 @@ pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
.signature()
.or_else(|_| git2::Signature::now("radicle", pk.to_string().as_str()))?;
let id_ref = format!("refs/remotes/{pk}/{}", &*RADICLE_ID_REF);
let id_ref = format!("refs/remotes/{pk}/{}", &*identity::doc::REFERENCE_NAME);
let _oid = repo.commit(Some(&id_ref), &sig, &sig, "Initialize Radicle", &tree, &[])?;
}
git::set_upstream(

View File

@ -7,7 +7,6 @@ use std::ops::Deref;
use std::path::Path;
use std::{fmt, io};
use radicle_git_ext as git_ext;
use thiserror::Error;
pub use radicle_git_ext::Oid;
@ -15,6 +14,7 @@ pub use radicle_git_ext::Oid;
use crate::collections::HashMap;
use crate::crypto;
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
use crate::git::ext as git_ext;
use crate::git::Url;
use crate::git::{RefError, RefStr, RefString};
use crate::identity;
@ -242,6 +242,8 @@ pub trait ReadRepository<'r> {
remote: &RemoteId,
reference: &RefStr,
) -> Result<Option<git2::Reference>, git2::Error>;
fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error>;
fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
fn reference_oid(
&self,
remote: &RemoteId,

View File

@ -20,7 +20,6 @@ use crate::storage::{
use super::{RefUpdate, RemoteId};
pub static RADICLE_ID_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("heads/radicle/id"));
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
Lazy::new(|| refspec::pattern!("refs/remotes/*"));
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
@ -257,21 +256,11 @@ impl Repository {
&self,
remote: &RemoteId,
) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
let oid = if let Some(oid) = self.reference_oid(remote, &RADICLE_ID_REF)? {
oid
if let Some((doc, _)) = identity::Doc::load(remote, self)? {
Ok(Some(doc.verified().unwrap()))
} else {
return Ok(None);
};
let doc = match self.blob_at(oid, Path::new(&*identity::doc::PATH)) {
Err(git::ext::Error::NotFound(_)) => return Ok(None),
Err(e) => return Err(e.into()),
Ok(doc) => doc,
};
let doc = identity::Doc::from_json(doc.content()).unwrap();
let doc = doc.verified().unwrap();
Ok(Some(doc))
Ok(None)
}
}
}
@ -287,7 +276,7 @@ impl<'r> ReadRepository<'r> for Repository {
self.backend.path()
}
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::ext::Error> {
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::Error> {
git::ext::Blob::At {
object: oid.into(),
path,
@ -311,6 +300,23 @@ impl<'r> ReadRepository<'r> for Repository {
})
}
fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error> {
self.backend.find_commit(oid.into()).map(Some).or_else(|e| {
if git::ext::is_not_found_err(&e) {
Ok(None)
} else {
Err(e)
}
})
}
fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
let mut revwalk = self.backend.revwalk()?;
revwalk.push(head.into())?;
Ok(revwalk)
}
fn reference_oid(
&self,
remote: &RemoteId,
@ -452,6 +458,46 @@ impl From<git2::Repository> for Repository {
}
}
pub mod trailers {
use std::str::FromStr;
use super::*;
use crate::crypto::{PublicKey, PublicKeyError};
use crate::crypto::{Signature, SignatureError};
pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
#[derive(Error, Debug)]
pub enum Error {
#[error("invalid format for signature trailer")]
SignatureTrailerFormat,
#[error("invalid public key in signature trailer")]
PublicKey(#[from] PublicKeyError),
#[error("invalid signature in trailer")]
Signature(#[from] SignatureError),
}
pub fn parse_signatures(msg: &str) -> Result<Vec<(PublicKey, Signature)>, Error> {
let trailers =
git2::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
let mut signatures = Vec::with_capacity(trailers.len());
for (key, val) in trailers.iter() {
if key == SIGNATURE_TRAILER {
if let Some((pk, sig)) = val.split_once(' ') {
let pk = PublicKey::from_str(pk)?;
let sig = Signature::from_str(sig)?;
signatures.push((pk, sig));
} else {
return Err(Error::SignatureTrailerFormat);
}
}
}
Ok(signatures)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -230,7 +230,7 @@ impl SignedRefs<Verified> {
{
let refs = repo.blob_at(oid, Path::new(REFS_BLOB_PATH))?;
let signature = repo.blob_at(oid, Path::new(SIGNATURE_BLOB_PATH))?;
let signature = crypto::Signature::try_from(signature.content())?;
let signature: crypto::Signature = signature.content().try_into()?;
match remote.verify(&signature, refs.content()) {
Ok(()) => {

View File

@ -1,6 +1,4 @@
use ed25519_consensus as ed25519;
use crate::crypto::{PublicKey, SecretKey, Signer};
use crate::crypto::{PublicKey, SecretKey, Signature, Signer};
#[derive(Debug, Clone)]
pub struct MockSigner {
@ -57,7 +55,7 @@ impl Signer for MockSigner {
&self.pk
}
fn sign(&self, msg: &[u8]) -> ed25519::Signature {
self.sk.sign(msg)
fn sign(&self, msg: &[u8]) -> Signature {
self.sk.sign(msg).into()
}
}

View File

@ -90,6 +90,14 @@ impl ReadRepository<'_> for MockRepository {
todo!()
}
fn commit(&self, _oid: git::Oid) -> Result<Option<git2::Commit>, git2::Error> {
todo!()
}
fn revwalk(&self, _head: git::Oid) -> Result<git2::Revwalk, git2::Error> {
todo!()
}
fn blob_at<'a>(
&'a self,
_oid: radicle_git_ext::Oid,