radicle: Test the signed refs grafting attack
This commit is contained in:
parent
46c2637f77
commit
24066c2600
|
|
@ -493,6 +493,21 @@ pub fn commit<'a>(
|
||||||
Ok(commit)
|
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.
|
/// Get the repository head.
|
||||||
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
|
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
|
||||||
let head = repo.head()?.peel_to_commit()?;
|
let head = repo.head()?.peel_to_commit()?;
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,11 @@ pub mod canonical {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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 qcheck_macros::quickcheck;
|
||||||
|
use storage::{git::transport, RemoteRepository, SignRepository, WriteStorage};
|
||||||
|
|
||||||
#[quickcheck]
|
#[quickcheck]
|
||||||
fn prop_canonical_roundtrip(refs: Refs) {
|
fn prop_canonical_roundtrip(refs: Refs) {
|
||||||
|
|
@ -470,4 +474,166 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(refs, decoded);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue