From 24066c260079b2a4bd282f081c3d5a079353af34 Mon Sep 17 00:00:00 2001 From: cloudhead Date: Wed, 16 Oct 2024 18:16:05 +0200 Subject: [PATCH] radicle: Test the signed refs grafting attack --- radicle/src/git.rs | 15 ++++ radicle/src/storage/refs.rs | 166 ++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/radicle/src/git.rs b/radicle/src/git.rs index 09fe8ced..beaafc74 100644 --- a/radicle/src/git.rs +++ b/radicle/src/git.rs @@ -493,6 +493,21 @@ pub fn commit<'a>( Ok(commit) } +/// Create an empty commit on top of the parent. +pub fn empty_commit<'a>( + repo: &'a git2::Repository, + parent: &'a git2::Commit, + target: &RefStr, + message: &str, + sig: &git2::Signature, +) -> Result, git2::Error> { + let tree = parent.tree()?; + let oid = repo.commit(Some(target.as_str()), sig, sig, message, &tree, &[parent])?; + let commit = repo.find_commit(oid)?; + + Ok(commit) +} + /// Get the repository head. pub fn head(repo: &git2::Repository) -> Result { let head = repo.head()?.peel_to_commit()?; diff --git a/radicle/src/storage/refs.rs b/radicle/src/storage/refs.rs index 15b8bdb7..060e8df7 100644 --- a/radicle/src/storage/refs.rs +++ b/radicle/src/storage/refs.rs @@ -461,7 +461,11 @@ pub mod canonical { #[cfg(test)] mod tests { use super::*; + use crate::assert_matches; + use crate::{cob::identity::Identity, rad, test::fixtures, Storage}; + use crypto::test::signer::MockSigner; use qcheck_macros::quickcheck; + use storage::{git::transport, RemoteRepository, SignRepository, WriteStorage}; #[quickcheck] fn prop_canonical_roundtrip(refs: Refs) { @@ -470,4 +474,166 @@ mod tests { assert_eq!(refs, decoded); } + + #[test] + #[should_panic] + // Test that a user's signed refs are tied to a specific RID, and they can't simply be + // used in a different repository. + // + // We create two repos, `paris` and `london`, and we copy over Bob's signed refs from `paris` + // to `london`. We expect that this does not cause the canonical head of the `london` repo + // to change, despite Bob being a delegate of both repos, because the refs were signed for the + // `paris` repo. We also don't expected the signed refs to validate without error. + fn test_rid_verification() { + let tmp = tempfile::tempdir().unwrap(); + let alice = MockSigner::default(); + let bob = MockSigner::default(); + let storage = &Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap(); + + transport::local::register(storage.clone()); + + // Alice creates "paris" repo. + let (paris_repo, paris_head) = fixtures::repository(tmp.path().join("paris")); + let (paris_rid, mut paris_doc, _) = rad::init( + &paris_repo, + "paris".try_into().unwrap(), + "Paris repository", + git::refname!("master"), + Default::default(), + &alice, + storage, + ) + .unwrap(); + + // Alice creates "london" repo. + let (london_repo, _london_head) = fixtures::repository(tmp.path().join("london")); + let (london_rid, mut london_doc, _) = rad::init( + &london_repo, + "london".try_into().unwrap(), + "London repository", + git::refname!("master"), + Default::default(), + &alice, + storage, + ) + .unwrap(); + + assert_ne!(london_rid, paris_rid); + + log::debug!(target: "test", "London RID: {london_rid}"); + log::debug!(target: "test", "Paris RID: {paris_rid}"); + + let paris = storage.repository_mut(paris_rid).unwrap(); + let london = storage.repository_mut(london_rid).unwrap(); + + // Bob is added to both repos as a delegate, by Alice. + { + paris_doc.delegates.push(bob.public_key().into()); + london_doc.delegates.push(bob.public_key().into()); + + let mut paris_ident = Identity::load_mut(&paris).unwrap(); + let mut london_ident = Identity::load_mut(&london).unwrap(); + + paris_ident + .update("Add Bob", "", &paris_doc, &alice) + .unwrap(); + london_ident + .update("Add Bob", "", &london_doc, &alice) + .unwrap(); + } + + // Now Bob checks out a copy of the `paris` repository and pushes a commit to the + // default branch (master). We store the OID of that commti in `bob_head`, as this + // is the commit we will try to get the `london` repo to point to. + let (bob_paris_sigrefs, bob_head) = { + let bob_working = rad::checkout( + paris.id, + bob.public_key(), + tmp.path().join("working"), + &storage, + ) + .unwrap(); + + let paris_head = bob_working.find_commit(paris_head).unwrap(); + let bob_sig = git2::Signature::now("bob", "bob@example.com").unwrap(); + let bob_head = git::empty_commit( + &bob_working, + &paris_head, + git::refname!("refs/heads/master").as_refstr(), + "Bob's commit", + &bob_sig, + ) + .unwrap(); + + let mut bob_master_ref = bob_working.find_reference("refs/heads/master").unwrap(); + bob_master_ref.set_target(bob_head.id(), "").unwrap(); + bob_working + .find_remote("rad") + .unwrap() + .push(&["refs/heads/master"], None) + .unwrap(); + let sigrefs = paris.sign_refs(&bob).unwrap(); + + assert_eq!( + sigrefs + .get(&git_ext::ref_format::qualified!("refs/heads/master")) + .unwrap(), + bob_head.id().into() + ); + (sigrefs, bob_head.id()) + }; + + { + // Sanity check: make sure the default branches don't already match between Alice and Bob. + let alice_paris_sigrefs = SignedRefsAt::load(*alice.public_key(), &paris) + .unwrap() + .unwrap(); + assert_ne!( + alice_paris_sigrefs + .get(&git_ext::ref_format::qualified!("refs/heads/master")) + .unwrap(), + bob_paris_sigrefs + .get(&git_ext::ref_format::qualified!("refs/heads/master")) + .unwrap() + ); + } + + { + // For the graft to work, we also have to copy over the objects that Bob created in + // `paris`, so that the grafted signed refs point to valid objects. + let paris_odb = paris.raw().odb().unwrap(); + let london_odb = london.raw().odb().unwrap(); + + paris_odb + .foreach(|oid| { + let obj = paris_odb.read(*oid).unwrap(); + london_odb.write(obj.kind(), obj.data()).unwrap(); + + true + }) + .unwrap(); + } + // Now we're going to "graft" Bob's signed refs from `paris` to `london`. + // We save Bob's `paris` signed refs in the `london` repo, performing the graft, and update + // Bob's `master` branch reference to point to his commit, created in the `paris` repo. This + // only modifies his own namespace. Note that anyone (eg. Eve) could create a reference + // under her copy of Bob's namespace, and this would only be rejected during signed ref + // validation. + let result = bob_paris_sigrefs.save(&london).unwrap(); + assert_matches!(result, Updated::Updated { .. }); + + london + .raw() + .reference( + git::refs::storage::branch_of(bob.public_key(), &git::refname!("master")).as_str(), + bob_head, + false, + "", + ) + .unwrap(); + + // Due to the verification, we get a validation error when trying to load Bob's remote. + // The graft is not allowed. + london.remote(bob.public_key()).unwrap_err(); + } }