verify perms, signature and DAG on commit

pull/19/head
Niko PLP 9 months ago
parent cd6eadf2a3
commit 04ceb0374e
  1. 2
      ng-app/src/wallet_emojis.ts
  2. 2
      ng-wallet/src/emojis.rs
  3. 20
      p2p-repo/src/branch.rs
  4. 383
      p2p-repo/src/commit.rs
  5. 24
      p2p-repo/src/errors.rs
  6. 17
      p2p-repo/src/repo.rs
  7. 2
      p2p-repo/src/store.rs
  8. 2
      p2p-repo/src/types.rs
  9. 6
      p2p-repo/src/utils.rs

@ -906,7 +906,7 @@ let face = [
{
hexcode: "1f37e",
shortcode: "bottle_with_popping_cork",
code: "champagne",
code: "champagne_bottle",
},
{
hexcode: "1f377",

@ -859,7 +859,7 @@ const food: [EmojiDef<'static>; 15] = [
EmojiDef {
hexcode: "1f37e",
shortcode: "bottle_with_popping_cork",
code: "champagne",
code: "champagne_bottle",
},
EmojiDef {
hexcode: "1f377",

@ -150,6 +150,21 @@ mod test {
//use fastbloom_rs::{BloomFilter as Filter, FilterBuilder, Membership};
struct Test<'a> {
storage: Box<dyn RepoStore + Send + Sync + 'a>,
}
impl<'a> Test<'a> {
fn storage(s: impl RepoStore + 'a) -> Self {
Test {
storage: Box::new(s),
}
}
fn s(&self) -> &Box<dyn RepoStore + Send + Sync + 'a> {
&self.storage
}
}
use crate::branch::*;
use crate::repo::Repo;
@ -266,7 +281,8 @@ mod test {
)
}
let store = Box::new(HashMapRepoStore::new());
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
// repo
@ -285,7 +301,7 @@ mod test {
&repo_pubkey,
&member_pubkey,
&[PermissionV0::WriteAsync],
store,
t.s(),
);
let repo_ref = ObjectRef {

@ -10,7 +10,7 @@
//! Commit
use core::fmt;
//use ed25519_dalek::*;
use ed25519_dalek::{PublicKey, Signature};
use once_cell::sync::OnceCell;
use crate::errors::NgError;
@ -25,7 +25,7 @@ use crate::utils::*;
use std::collections::HashSet;
use std::iter::FromIterator;
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum CommitLoadError {
MissingBlocks(Vec<BlockId>),
ObjectParseError,
@ -33,16 +33,16 @@ pub enum CommitLoadError {
NotACommitBodyError,
CannotBeAtRootOfBranch,
MustBeAtRootOfBranch,
BodyLoadError,
SingletonCannotHaveHeader,
BodyLoadError(Vec<BlockId>),
HeaderLoadError,
BodyTypeMismatch,
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum CommitVerifyError {
InvalidSignature,
PermissionDenied,
BodyLoadError(CommitLoadError),
DepLoadError(CommitLoadError),
}
@ -64,7 +64,7 @@ impl CommitV0 {
body: ObjectRef,
) -> Result<CommitV0, NgError> {
let headers = CommitHeader::new_with(deps, ndeps, acks, nacks, refs, nrefs);
let content = CommitContentV0 {
let content = CommitContent::V0(CommitContentV0 {
perms: vec![],
author: (&author_pubkey).into(),
seq,
@ -73,13 +73,13 @@ impl CommitV0 {
quorum,
metadata,
body,
};
});
let content_ser = serde_bare::to_vec(&content).unwrap();
// sign commit
let sig = sign(&author_privkey, &author_pubkey, &content_ser)?;
Ok(CommitV0 {
content: CommitContent::V0(content),
content: content,
sig,
id: None,
key: None,
@ -124,6 +124,67 @@ impl Commit {
.map(|c| Commit::V0(c))
}
/// New commit
pub fn new_with_body_and_save(
author_privkey: PrivKey,
author_pubkey: PubKey,
seq: u64,
branch: BranchId,
quorum: QuorumType,
deps: Vec<ObjectRef>,
ndeps: Vec<ObjectRef>,
acks: Vec<ObjectRef>,
nacks: Vec<ObjectRef>,
refs: Vec<ObjectRef>,
nrefs: Vec<ObjectRef>,
metadata: Vec<u8>,
body: CommitBody,
block_size: usize,
store_pubkey: &StoreRepo,
store_secret: &ReadCapSecret,
store: &Box<impl RepoStore + ?Sized>,
) -> Result<Commit, NgError> {
let body_ref = body
.clone()
.save(block_size, store_pubkey, store_secret, store)?;
let mut commit = CommitV0::new(
author_privkey,
author_pubkey,
seq,
branch,
quorum,
deps,
ndeps,
acks,
nacks,
refs,
nrefs,
metadata,
body_ref,
)
.map(|c| Commit::V0(c))?;
commit.set_body(body);
let commit_ref = commit.save(block_size, store_pubkey, store_secret, store)?;
commit.set_id(commit_ref.id);
commit.set_key(commit_ref.key);
Ok(commit)
}
pub fn reference(&self) -> Option<ObjectRef> {
if self.key().is_some() && self.id().is_some() {
Some(ObjectRef {
id: self.id().unwrap(),
key: self.key().unwrap(),
})
} else {
None
}
}
pub fn save(
&mut self,
block_size: usize,
@ -133,6 +194,12 @@ impl Commit {
) -> Result<ObjectRef, StorageError> {
match self {
Commit::V0(v0) => {
if v0.id.is_some() && v0.key.is_some() {
return Ok(ObjectRef::from_id_key(
v0.id.unwrap(),
v0.key.as_ref().unwrap().clone(),
));
}
log_debug!("{:?}", v0.header);
let mut obj = Object::new(
ObjectContent::V0(ObjectContentV0::Commit(Commit::V0(v0.clone()))),
@ -198,7 +265,7 @@ impl Commit {
let content = self.content_v0();
let (id, key) = (content.body.id, content.body.key.clone());
let obj = Object::load(id.clone(), Some(key.clone()), store).map_err(|e| match e {
ObjectParseError::MissingBlocks(missing) => CommitLoadError::MissingBlocks(missing),
ObjectParseError::MissingBlocks(missing) => CommitLoadError::BodyLoadError(missing),
_ => CommitLoadError::ObjectParseError,
})?;
let content = obj
@ -430,29 +497,51 @@ impl Commit {
}
/// Verify commit signature
pub fn verify_sig(&self) -> Result<(), NgError> {
pub fn verify_sig(&self, repo: &Repo) -> Result<(), CommitVerifyError> {
let c = match self {
Commit::V0(c) => c,
};
let content_ser = serde_bare::to_vec(&c.content).unwrap();
unimplemented!();
// FIXME : lookup author in member's list
// let pubkey = match c.content.author() {
// PubKey::Ed25519PubKey(pk) => pk,
// _ => panic!("author cannot have a Montgomery key"),
// };
// let pk = PublicKey::from_bytes(pubkey)?;
// let sig_bytes = match c.sig {
// Sig::Ed25519Sig(ss) => [ss[0], ss[1]].concat(),
// };
// let sig = Signature::from_bytes(&sig_bytes)?;
// pk.verify_strict(&content_ser, &sig)
let pubkey = repo
.member_pubkey(c.content.author())
.map_err(|_| CommitVerifyError::PermissionDenied)?;
let pubkey_slice = match pubkey {
PubKey::Ed25519PubKey(pk) => pk,
_ => panic!("author cannot have a Montgomery key"),
};
let pk = PublicKey::from_bytes(&pubkey_slice)
.map_err(|_| CommitVerifyError::InvalidSignature)?;
let sig_bytes = match c.sig {
Sig::Ed25519Sig(ss) => [ss[0], ss[1]].concat(),
};
let sig =
Signature::from_bytes(&sig_bytes).map_err(|_| CommitVerifyError::InvalidSignature)?;
pk.verify_strict(&content_ser, &sig)
.map_err(|_| CommitVerifyError::InvalidSignature)
}
/// Verify commit permissions
pub fn verify_perm(&self, repo: &Repo) -> Result<(), CommitVerifyError> {
pub fn verify_perm(&self, repo: &Repo) -> Result<(), NgError> {
repo.verify_permission(self)
.map_err(|_| CommitVerifyError::PermissionDenied)
}
pub fn verify_perm_creation(&self, user: Option<&Digest>) -> Result<&Digest, NgError> {
let digest = self.content().author();
if user.is_some() && *digest != *user.unwrap() {
return Err(NgError::PermissionDenied);
}
let body = self.body().ok_or(NgError::InvalidArgument)?;
if !(body.is_repository_singleton_commit() && user.is_none()) {
// a user must be provided to verify all subsequent commits of a Repository commit, that have the same author and that are signed with the repository key
return Err(NgError::InvalidArgument);
}
if body.required_permission().contains(&PermissionV0::Create) {
Ok(digest)
} else {
Err(NgError::PermissionDenied)
}
}
/// Verify if the commit's `body` and its direct_causal_past, and recursively all their refs are available in the `store`
@ -498,8 +587,8 @@ impl Commit {
Ok(_) => Ok(()),
Err(CommitLoadError::MissingBlocks(m)) => {
// The commit body is missing.
missing.extend(m);
Err(CommitLoadError::BodyLoadError)
missing.extend(m.clone());
Err(CommitLoadError::BodyLoadError(m))
}
Err(e) => Err(e),
}?;
@ -510,6 +599,9 @@ impl Commit {
if !body.must_be_root_commit_in_branch() {
return Err(CommitLoadError::CannotBeAtRootOfBranch);
}
if body.is_repository_singleton_commit() && commit.header().is_some() {
return Err(CommitLoadError::SingletonCannotHaveHeader);
}
} else {
if body.must_be_root_commit_in_branch() {
return Err(CommitLoadError::MustBeAtRootOfBranch);
@ -543,12 +635,10 @@ impl Commit {
}
/// Verify signature, permissions, and full causal past
pub fn verify(&self, repo: &Repo) -> Result<(), CommitVerifyError> {
self.verify_sig()
.map_err(|_e| CommitVerifyError::InvalidSignature)?;
pub fn verify(&self, repo: &Repo) -> Result<(), NgError> {
self.verify_sig(repo)?;
self.verify_perm(repo)?;
self.verify_full_object_refs_of_branch_at_commit(repo.get_store())
.map_err(|e| CommitVerifyError::DepLoadError(e))?;
self.verify_full_object_refs_of_branch_at_commit(repo.get_store())?;
Ok(())
}
}
@ -588,6 +678,24 @@ impl PermissionV0 {
}
impl CommitBody {
pub fn save(
self,
block_size: usize,
store_pubkey: &StoreRepo,
store_secret: &ReadCapSecret,
store: &Box<impl RepoStore + ?Sized>,
) -> Result<ObjectRef, StorageError> {
let obj = Object::new(
ObjectContent::V0(ObjectContentV0::CommitBody(self)),
None,
block_size,
store_pubkey,
store_secret,
);
obj.save(store)?;
Ok(obj.reference().unwrap())
}
pub fn root_branch_commit(&self) -> Result<&RootBranch, CommitLoadError> {
match self {
Self::V0(v0) => match v0 {
@ -1009,7 +1117,7 @@ impl fmt::Display for Commit {
writeln!(f, "== Sig: {}", v0.sig)?;
write!(f, "{}", v0.content)?;
if v0.body.get().is_some() {
writeln!(f, "== Body: {}", v0.body.get().unwrap())?;
write!(f, "== Body: {}", v0.body.get().unwrap())?;
}
}
}
@ -1127,6 +1235,21 @@ mod test {
use crate::commit::*;
use crate::log::*;
struct Test<'a> {
storage: Box<dyn RepoStore + Send + Sync + 'a>,
}
impl<'a> Test<'a> {
fn storage(s: impl RepoStore + 'a) -> Self {
Test {
storage: Box::new(s),
}
}
fn s(&self) -> &Box<dyn RepoStore + Send + Sync + 'a> {
&self.storage
}
}
fn test_commit_header_ref_content_fits(
obj_refs: Vec<BlockRef>,
metadata_size: usize,
@ -1173,8 +1296,12 @@ mod test {
.save(max_object_size, &store_repo, &store_secret, &storage)
.expect("save commit");
let commit_object = Object::load(commit_ref.id, Some(commit_ref.key), &storage)
.expect("load object from storage");
let commit_object = Object::load(
commit_ref.id.clone(),
Some(commit_ref.key.clone()),
&storage,
)
.expect("load object from storage");
assert_eq!(
commit_object.acks(),
@ -1186,6 +1313,10 @@ mod test {
log_debug!("object size: {}", commit_object.size());
assert_eq!(commit_object.all_blocks_len(), expect_blocks_len);
let commit = Commit::load(commit_ref, &storage, false).expect("load commit from storage");
log_debug!("{}", commit);
}
#[test]
@ -1206,7 +1337,96 @@ mod test {
}
#[test]
pub fn test_commit() {
pub fn test_load_commit_fails_on_non_commit_object() {
let file = File::V0(FileV0 {
content_type: "file/test".into(),
metadata: Vec::from("some meta data here"),
content: [(0..255).collect::<Vec<u8>>().as_slice(); 320].concat(),
});
let content = ObjectContent::V0(ObjectContentV0::File(file));
let max_object_size = 0;
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
let obj = Object::new(
content.clone(),
None,
max_object_size,
&store_repo,
&store_secret,
);
let hashmap_storage = HashMapRepoStore::new();
let storage = Box::new(hashmap_storage);
obj.save(&storage).expect("save object");
let commit = Commit::load(obj.reference().unwrap(), &storage, false);
assert_eq!(commit, Err(CommitLoadError::NotACommitError));
}
#[test]
pub fn test_load_commit_with_body() {
let (priv_key, pub_key) = generate_keypair();
let seq = 3;
let obj_ref = ObjectRef::dummy();
let branch = pub_key;
let obj_refs = vec![obj_ref.clone()];
let deps = obj_refs.clone();
let acks = obj_refs.clone();
let refs = obj_refs.clone();
let metadata = Vec::from("some metadata");
let body = CommitBody::V0(CommitBodyV0::Repository(Repository::V0(RepositoryV0 {
id: branch,
verification_program: vec![],
creator: None,
metadata: vec![],
})));
let max_object_size = 0;
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
let hashmap_storage = HashMapRepoStore::new();
let storage = Box::new(hashmap_storage);
let commit = Commit::new_with_body_and_save(
priv_key,
pub_key,
seq,
branch,
QuorumType::NoSigning,
deps,
vec![],
acks.clone(),
vec![],
refs,
vec![],
metadata,
body,
max_object_size,
&store_repo,
&store_secret,
&storage,
)
.expect("commit::new_With_body_and_save");
log_debug!("{}", commit);
let commit2 = Commit::load(commit.reference().unwrap(), &storage, true)
.expect("load commit with body after save");
log_debug!("{}", commit2);
assert_eq!(commit, commit2);
}
#[test]
pub fn test_commit_load_body_fails() {
let (priv_key, pub_key) = generate_keypair();
let seq = 3;
let obj_ref = ObjectRef::dummy();
@ -1236,24 +1456,31 @@ mod test {
.unwrap();
log_debug!("{}", commit);
let store = Box::new(HashMapRepoStore::new());
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let repo = Repo::new_with_member(&pub_key, &pub_key, &[PermissionV0::WriteAsync], store);
let repo = Repo::new_with_member(&pub_key, &pub_key, &[PermissionV0::Create], t.s());
match commit.load_body(repo.get_store()) {
Ok(_b) => panic!("Body should not exist"),
Err(CommitLoadError::MissingBlocks(missing)) => {
Err(CommitLoadError::BodyLoadError(missing)) => {
assert_eq!(missing.len(), 1);
}
Err(e) => panic!("Commit verify error: {:?}", e),
Err(e) => panic!("Commit load error: {:?}", e),
}
commit.verify_sig().expect("Invalid signature");
commit.verify_perm(&repo).expect("Permission denied");
commit.verify_sig(&repo).expect("verify signature");
match commit.verify_perm(&repo) {
Ok(_) => panic!("Commit should not be Ok"),
Err(NgError::CommitLoadError(CommitLoadError::BodyLoadError(missing))) => {
assert_eq!(missing.len(), 1);
}
Err(e) => panic!("Commit verify perm error: {:?}", e),
}
match commit.verify_full_object_refs_of_branch_at_commit(repo.get_store()) {
Ok(_) => panic!("Commit should not be Ok"),
Err(CommitLoadError::MissingBlocks(missing)) => {
Err(CommitLoadError::BodyLoadError(missing)) => {
assert_eq!(missing.len(), 1);
}
Err(e) => panic!("Commit verify error: {:?}", e),
@ -1261,10 +1488,80 @@ mod test {
match commit.verify(&repo) {
Ok(_) => panic!("Commit should not be Ok"),
Err(CommitVerifyError::BodyLoadError(CommitLoadError::MissingBlocks(missing))) => {
Err(NgError::CommitLoadError(CommitLoadError::BodyLoadError(missing))) => {
assert_eq!(missing.len(), 1);
}
Err(e) => panic!("Commit verify error: {:?}", e),
}
}
#[test]
pub fn test_load_commit_with_body_verify_perms() {
let (priv_key, pub_key) = generate_keypair();
let seq = 3;
let obj_ref = ObjectRef::dummy();
let branch = pub_key;
let obj_refs = vec![obj_ref.clone()];
let deps = obj_refs.clone();
let acks = obj_refs.clone();
let refs = obj_refs.clone();
let metadata = Vec::from("some metadata");
let body = CommitBody::V0(CommitBodyV0::Repository(Repository::V0(RepositoryV0 {
id: branch,
verification_program: vec![],
creator: None,
metadata: vec![],
})));
let max_object_size = 0;
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
let hashmap_storage = HashMapRepoStore::new();
let storage = Box::new(hashmap_storage);
let commit = Commit::new_with_body_and_save(
priv_key,
pub_key,
seq,
branch,
QuorumType::NoSigning,
vec![],
vec![],
vec![], //acks.clone(),
vec![],
vec![],
vec![],
metadata,
body,
max_object_size,
&store_repo,
&store_secret,
&storage,
)
.expect("commit::new_with_body_and_save");
log_debug!("{}", commit);
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let repo = Repo::new_with_member(&pub_key, &pub_key, &[PermissionV0::Create], t.s());
commit.load_body(repo.get_store()).expect("load body");
commit.verify_sig(&repo).expect("verify signature");
commit.verify_perm(&repo).expect("verify perms");
commit
.verify_perm_creation(None)
.expect("verify_perm_creation");
commit
.verify_full_object_refs_of_branch_at_commit(repo.get_store())
.expect("verify is at root of branch and singleton");
commit.verify(&repo).expect("verify");
}
}

@ -9,7 +9,8 @@
//! Errors
use crate::commit::CommitLoadError;
use crate::commit::{CommitLoadError, CommitVerifyError};
use crate::store::StorageError;
use crate::types::BlockId;
use core::fmt;
use std::error::Error;
@ -25,7 +26,10 @@ pub enum NgError {
InvalidFileFormat,
InvalidArgument,
PermissionDenied,
RepoLoadError,
CommitLoadError(CommitLoadError),
StorageError(StorageError),
NotFound,
CommitVerifyError(CommitVerifyError),
}
impl Error for NgError {}
@ -49,8 +53,20 @@ impl From<ed25519_dalek::ed25519::Error> for NgError {
}
impl From<CommitLoadError> for NgError {
fn from(_e: CommitLoadError) -> Self {
NgError::RepoLoadError
fn from(e: CommitLoadError) -> Self {
NgError::CommitLoadError(e)
}
}
impl From<CommitVerifyError> for NgError {
fn from(e: CommitVerifyError) -> Self {
NgError::CommitVerifyError(e)
}
}
impl From<StorageError> for NgError {
fn from(e: StorageError) -> Self {
NgError::StorageError(e)
}
}

@ -10,6 +10,7 @@
//! Repository serde implementation and in memory helper
use crate::errors::*;
use crate::log::*;
use crate::store::*;
use crate::types::*;
@ -41,12 +42,15 @@ pub struct UserInfo {
impl UserInfo {
pub fn has_any_perm(&self, perms: &HashSet<PermissionV0>) -> Result<(), NgError> {
//log_debug!("perms {:?}", perms);
if self.has_perm(&PermissionV0::Owner).is_ok() {
return Ok(());
}
let is_admin = self.has_perm(&PermissionV0::Admin).is_ok();
//log_debug!("is_admin {}", is_admin);
//is_delegated_by_admin
let has_perms: HashSet<&PermissionV0> = self.permissions.keys().collect();
//log_debug!("has_perms {:?}", has_perms);
for perm in perms {
if is_admin && perm.is_delegated_by_admin() || has_perms.contains(perm) {
return Ok(());
@ -69,7 +73,7 @@ pub struct Repo<'a> {
pub members: HashMap<Digest, UserInfo>,
store: Box<dyn RepoStore + Send + Sync + 'a>,
store: &'a Box<dyn RepoStore + Send + Sync + 'a>,
}
impl<'a> Repo<'a> {
@ -77,7 +81,7 @@ impl<'a> Repo<'a> {
id: &PubKey,
member: &UserId,
perms: &[PermissionV0],
store: Box<dyn RepoStore + Send + Sync + 'a>,
store: &'a Box<dyn RepoStore + Send + Sync + 'a>,
) -> Self {
let mut members = HashMap::new();
let permissions = HashMap::from_iter(
@ -112,7 +116,14 @@ impl<'a> Repo<'a> {
Err(NgError::PermissionDenied)
}
pub fn member_pubkey(&self, hash: &Digest) -> Result<UserId, NgError> {
match self.members.get(hash) {
Some(user_info) => Ok(user_info.id),
None => Err(NgError::NotFound),
}
}
pub fn get_store(&self) -> &Box<dyn RepoStore + Send + Sync + 'a> {
&self.store
self.store
}
}

@ -36,7 +36,7 @@ pub trait RepoStore: Send + Sync {
fn len(&self) -> Result<usize, StorageError>;
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum StorageError {
NotFound,
InvalidValue,

@ -1659,7 +1659,7 @@ pub struct CommitV0 {
/// Commit content
pub content: CommitContent,
/// Signature over the content by the author. an editor (userId)
/// Signature over the content (a CommitContent) by the author. an editor (userId)
pub sig: Sig,
}

@ -125,6 +125,12 @@ pub fn sign(
) -> Result<Sig, NgError> {
let keypair = pubkey_privkey_to_keypair(author_pubkey, author_privkey);
let sig_bytes = keypair.sign(content.as_slice()).to_bytes();
// log_debug!(
// "XXXX SIGN {:?} {:?} {:?}",
// author_pubkey,
// content.as_slice(),
// sig_bytes
// );
let mut it = sig_bytes.chunks_exact(32);
let mut ss: Ed25519Sig = [[0; 32], [0; 32]];
ss[0].copy_from_slice(it.next().unwrap());

Loading…
Cancel
Save