radicle: Test the signed refs grafting attack

This commit is contained in:
cloudhead 2024-10-16 18:16:05 +02:00
parent 46c2637f77
commit 24066c2600
No known key found for this signature in database
2 changed files with 181 additions and 0 deletions

View File

@ -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::Commit<'a>, 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<git2::Commit, git2::Error> {
let head = repo.head()?.peel_to_commit()?;

View File

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