Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Include `rad/id` in signed refs
Merged did:key:z6MksFqX...wzpT opened 1 year ago

We ensure that a rad/id ref is included in the signed refs file under rad/sigrefs. This prevents a certain kind of “grafting” attack where signed refs can be copied between repositories.

When verifying signed refs, we ensure that the ref is present and points to an identity branch that matches the repository identity containing the signed refs.

23 files changed +415 -132 46c2637f 989edacd
modified radicle-cli/examples/rad-clean.md
@@ -18,8 +18,8 @@ Let's also inspect what remotes are in the repository:

``` ~alice
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f209c9f68aa689af24220a20462e13ee9dfb2a95
-
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk 161b775a3509c8098de67f57f750972bba015b31
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 99c549702e2bcfe02b0e68d4a2224fb7a1524529
+
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk e9f48ef90fe8592e1b1c95f96c21a59ca1495300
```

Now let's clean the `heartwood` project:
@@ -34,7 +34,7 @@ Inspecting the remotes again, we see that Bob is now gone:

``` ~alice
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f209c9f68aa689af24220a20462e13ee9dfb2a95
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 99c549702e2bcfe02b0e68d4a2224fb7a1524529
```

Note that Bob will be fetched again if we do not untrack his
modified radicle-cli/examples/rad-clone-all.md
@@ -36,12 +36,14 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │   └── master
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
└── refs
    ├── heads
    │   └── master
    └── rad
+
        ├── root
        └── sigrefs
```

@@ -75,17 +77,20 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │   └── master
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
└── refs
    ├── heads
    │   └── master
    └── rad
+
        ├── root
        └── sigrefs
z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
└── refs
    ├── heads
    │   └── master
    └── rad
+
        ├── root
        └── sigrefs
```
modified radicle-cli/examples/rad-fork.md
@@ -14,6 +14,7 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │   └── master
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
```

@@ -39,12 +40,14 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │   └── master
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
└── refs
    ├── heads
    │   └── master
    └── rad
+
        ├── root
        └── sigrefs
```

modified radicle-cli/examples/rad-id-multi-delegate.md
@@ -4,7 +4,7 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --des
```

``` ~bob
-
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t 95cd447c57de8d232c6154f5dba0451aa593520e -i 500 --timeout 5000
+
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t c9a828fc2fb01f893d6e6e9e17b9092dea2b3aba -i 500 --timeout 5000
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Fetched repository from 1 seed(s)
modified radicle-cli/examples/rad-id-threshold-soft-fork.md
@@ -29,6 +29,7 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │   └── master
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
└── refs
@@ -36,6 +37,7 @@ z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
    │   └── xyz.radicle.issue
    │       └── f12d512c51d30429f7916db038ae0360e2e938c2
    └── rad
+
        ├── root
        └── sigrefs
```

modified radicle-cli/examples/rad-id-threshold.md
@@ -141,6 +141,7 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │       └── b09b2aa0ee055671c811e9ad4ba73eed211ebaa3
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
```

@@ -148,7 +149,7 @@ Similarly, she still does not have Bob's `rad/sigrefs`:

``` ~alice
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 7ffed605e4871bb0640ee1538181640e239b182c
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8
```

And she can still list the project, without any worries:
@@ -195,6 +196,6 @@ $ rad sync -f
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
✓ Fetched repository from 2 seed(s)
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 7ffed605e4871bb0640ee1538181640e239b182c
-
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk cc068d93ee77dc134518d7d0fbe55b39804baf53
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8
+
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk dace6fe948548168a2bb687718949d9b5d9076ee
```
modified radicle-cli/examples/rad-inspect.md
@@ -26,6 +26,7 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │   └── master
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
```

@@ -33,7 +34,7 @@ And sigrefs:

```
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f209c9f68aa689af24220a20462e13ee9dfb2a95
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 99c549702e2bcfe02b0e68d4a2224fb7a1524529
```

Or display the repository identity's payload and delegates:
modified radicle-cli/examples/rad-merge-via-push.md
@@ -45,6 +45,7 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │       └── 696ec5508494692899337afe6713fe1796d0315c
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
```

@@ -115,5 +116,6 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │   └── master
    └── rad
        ├── id
+
        ├── root
        └── sigrefs
```
modified radicle-cli/examples/rad-sync.md
@@ -15,9 +15,9 @@ $ rad sync status --sort-by alias
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   Node                      Address                      Status        Tip       Timestamp │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   alice   (you)             alice.radicle.example:8776   unannounced   a9ce0d1   [  ...  ] │
-
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     out-of-sync   f209c9f   [  ...  ] │
-
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     out-of-sync   f209c9f   [  ...  ] │
+
│ ●   alice   (you)             alice.radicle.example:8776   unannounced   056b1db   [  ...  ] │
+
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     out-of-sync   99c5497   [  ...  ] │
+
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     out-of-sync   99c5497   [  ...  ] │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -37,9 +37,9 @@ $ rad sync status --sort-by alias
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   Node                      Address                      Status   Tip       Timestamp │
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   alice   (you)             alice.radicle.example:8776            a9ce0d1   [  ...  ] │
-
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     synced   a9ce0d1   [  ...  ] │
-
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     synced   a9ce0d1   [  ...  ] │
+
│ ●   alice   (you)             alice.radicle.example:8776            056b1db   [  ...  ] │
+
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     synced   056b1db   [  ...  ] │
+
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     synced   056b1db   [  ...  ] │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

modified radicle-node/src/test/peer.rs
@@ -13,7 +13,7 @@ use radicle::node::Database;
use radicle::node::UserAgent;
use radicle::node::{address, Alias, ConnectOptions};
use radicle::rad;
-
use radicle::storage::refs::{RefsAt, SignedRefsAt};
+
use radicle::storage::refs::{RefsAt, SignedRefsAt, IDENTITY_ROOT};
use radicle::storage::{ReadRepository, RemoteRepository};
use radicle::Storage;

@@ -353,9 +353,15 @@ where
        ann.into().signed(self.signer()).into()
    }

-
    pub fn signed_refs_at(&self, refs: Refs, at: radicle::git::Oid) -> SignedRefsAt {
+
    pub fn signed_refs_at<R: ReadRepository>(
+
        &self,
+
        mut refs: Refs,
+
        at: radicle::git::Oid,
+
        repo: &R,
+
    ) -> SignedRefsAt {
+
        refs.insert(IDENTITY_ROOT.to_ref_string(), repo.identity_root().unwrap());
        SignedRefsAt {
-
            sigrefs: refs.signed(self.signer()).unwrap(),
+
            sigrefs: refs.signed(self.signer()).unwrap().verified(repo).unwrap(),
            at,
        }
    }
modified radicle-node/src/tests.rs
@@ -877,13 +877,15 @@ fn test_refs_announcement_followed() {
    let mut bob = Peer::with_storage("bob", [8, 8, 8, 8], storage_bob);

    let node_id = alice.id;
-
    alice.storage_mut().repo_mut(&rid).remotes.insert(
+
    let repo = alice.storage_mut().repo_mut(&rid);
+

+
    repo.remotes.insert(
        node_id,
-
        bob.signed_refs_at(arbitrary::gen::<Refs>(8), arbitrary::oid()),
+
        bob.signed_refs_at(arbitrary::gen::<Refs>(8), arbitrary::oid(), repo),
    );

    // Generate some refs for Bob under their own node_id.
-
    let sigrefs = bob.signed_refs_at(arbitrary::gen::<Refs>(8), arbitrary::oid());
+
    let sigrefs = bob.signed_refs_at(arbitrary::gen::<Refs>(8), arbitrary::oid(), repo);
    let node_id = bob.id;
    bob.init();
    bob.storage_mut()
@@ -1544,9 +1546,10 @@ fn test_queued_fetch_from_ann_same_rid() {

    // Finish the 1st fetch.
    // Ensure the ref is in the storage and cache.
-
    alice.storage_mut().repo_mut(&rid).remotes.insert(
+
    let repo = alice.storage_mut().repo_mut(&rid);
+
    repo.remotes.insert(
        carol.id(),
-
        carol.signed_refs_at(arbitrary::gen::<Refs>(1), oid),
+
        carol.signed_refs_at(arbitrary::gen::<Refs>(1), oid, repo),
    );
    alice
        .database_mut()
modified radicle-node/src/worker/fetch.rs
@@ -210,6 +210,11 @@ fn notify(
                // be a separate notification on the identity COB itself.
                continue;
            }
+
            if r == *git::refs::storage::IDENTITY_ROOT {
+
                // Don't notify about the peers's identity root pointer. This is only used
+
                // for sigref verification.
+
                continue;
+
            }
            if let Some(rest) = r.strip_prefix(git::refname!("refs/heads/patches")) {
                if radicle::cob::ObjectId::from_str(rest.as_str()).is_ok() {
                    // Don't notify about patch branches, since we already get
modified radicle/src/cob/identity.rs
@@ -86,7 +86,7 @@ pub enum ApplyError {
    /// This error indicates that the operations are not being applied
    /// in causal order, which is a requirement for this CRDT.
    ///
-
    /// For example, this can occur if an operation references anothern operation
+
    /// For example, this can occur if an operation references another operation
    /// that hasn't happened yet.
    #[error("causal dependency {0:?} missing")]
    Missing(EntryId),
modified radicle/src/cob/patch.rs
@@ -341,7 +341,7 @@ impl<'a, R: WriteRepository> Merged<'a, R> {
        self,
        working: &git::raw::Repository,
        signer: &G,
-
    ) -> Result<(), storage::Error> {
+
    ) -> Result<(), storage::RepositoryError> {
        let nid = signer.public_key();
        let stored_ref = git::refs::patch(&self.patch).with_namespace(nid.into());
        let working_ref = git::refs::workdir::patch_upstream(&self.patch);
modified radicle/src/cob/store.rs
@@ -91,7 +91,7 @@ pub enum Error {
    #[error("object `{1}` of type `{0}` was not found")]
    NotFound(TypeName, ObjectId),
    #[error("signed refs: {0}")]
-
    SignRefs(#[from] storage::Error),
+
    SignRefs(Box<storage::RepositoryError>),
    #[error("invalid or unknown embed URI: {0}")]
    EmbedUri(Uri),
    #[error(transparent)]
@@ -181,7 +181,9 @@ where
                changes,
            },
        )?;
-
        self.repo.sign_refs(signer).map_err(Error::SignRefs)?;
+
        self.repo
+
            .sign_refs(signer)
+
            .map_err(|e| Error::SignRefs(Box::new(e)))?;

        Ok(updated)
    }
@@ -220,8 +222,14 @@ where
                contents,
            },
        )?;
-
        self.repo.sign_refs(signer).map_err(Error::SignRefs)?;
-

+
        // Nb. We can't sign our refs before the identity refs exist, which are created after
+
        // the identity COB is created. Therefore we manually sign refs when creating identity
+
        // COBs.
+
        if T::type_name() != &*crate::cob::identity::TYPENAME {
+
            self.repo
+
                .sign_refs(signer)
+
                .map_err(|e| Error::SignRefs(Box::new(e)))?;
+
        }
        Ok((*cob.id(), cob.object))
    }

@@ -234,7 +242,9 @@ where
        {
            Ok(_) => {
                cob::remove(self.repo, signer.public_key(), T::type_name(), id)?;
-
                self.repo.sign_refs(signer).map_err(Error::SignRefs)?;
+
                self.repo
+
                    .sign_refs(signer)
+
                    .map_err(|e| Error::SignRefs(Box::new(e)))?;
                Ok(())
            }
            Err(err) if err.code() == git::raw::ErrorCode::NotFound => Ok(()),
modified radicle/src/git.rs
@@ -184,7 +184,7 @@ pub mod refs {

        use super::*;

-
        /// Where the project's identity document is stored.
+
        /// Where the repo's identity document is stored.
        ///
        /// `refs/rad/id`
        ///
@@ -192,6 +192,14 @@ pub mod refs {
            Qualified::from_components(name::component!("rad"), name::component!("id"), None)
        });

+
        /// Where the repo's identity root document is stored.
+
        ///
+
        /// `refs/rad/root`
+
        ///
+
        pub static IDENTITY_ROOT: Lazy<Qualified> = Lazy::new(|| {
+
            Qualified::from_components(name::component!("rad"), name::component!("root"), None)
+
        });
+

        /// Where the project's signed references are stored.
        ///
        /// `refs/rad/sigrefs`
@@ -250,6 +258,14 @@ pub mod refs {
            IDENTITY_BRANCH.with_namespace(remote.into())
        }

+
        /// Get the root of the branch where the project's identity document is stored.
+
        ///
+
        /// `refs/namespaces/<remote>/refs/rad/root`
+
        ///
+
        pub fn id_root(remote: &RemoteId) -> Namespaced {
+
            IDENTITY_ROOT.with_namespace(remote.into())
+
        }
+

        /// Get the branch where the `remote`'s signed references are
        /// stored.
        ///
@@ -493,6 +509,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()?;
modified radicle/src/rad.rs
@@ -1,4 +1,5 @@
#![allow(clippy::let_unit_value)]
+
#![warn(clippy::unwrap_used)]
use std::io;
use std::path::Path;
use std::str::FromStr;
@@ -54,8 +55,7 @@ pub fn init<G: Signer, S: WriteStorage>(
    storage: S,
) -> Result<(RepoId, identity::Doc<Verified>, SignedRefs<Verified>), InitError> {
    // TODO: Better error when project id already exists in storage, but remote doesn't.
-
    let pk = signer.public_key();
-
    let delegate = identity::Did::from(*pk);
+
    let delegate: identity::Did = signer.public_key().into();
    let proj = Project::new(
        name.to_owned(),
        description.to_owned(),
@@ -70,10 +70,10 @@ pub fn init<G: Signer, S: WriteStorage>(
        )
    })?;
    let doc = identity::Doc::initial(proj, delegate, visibility).verified()?;
-
    let (project, _) = Repository::init(&doc, &storage, signer)?;
+
    let (project, identity) = Repository::init(&doc, &storage, signer)?;
    let url = git::Url::from(project.id);

-
    match init_configure(repo, &project, pk, &default_branch, &url, signer) {
+
    match init_configure(repo, &project, &default_branch, &url, identity, signer) {
        Ok(signed) => Ok((project.id, doc, signed)),
        Err(err) => {
            if let Err(e) = project.remove() {
@@ -91,15 +91,17 @@ pub fn init<G: Signer, S: WriteStorage>(

fn init_configure<G>(
    repo: &git2::Repository,
-
    project: &Repository,
-
    pk: &crypto::PublicKey,
+
    stored: &Repository,
    default_branch: &BranchName,
    url: &git::Url,
+
    identity: git::Oid,
    signer: &G,
) -> Result<SignedRefs<Verified>, InitError>
where
    G: crypto::Signer,
{
+
    let pk = signer.public_key();
+

    git::configure_repository(repo)?;
    git::configure_remote(repo, &REMOTE_NAME, url, &url.clone().with_namespace(*pk))?;
    git::push(
@@ -110,10 +112,11 @@ where
            &git::fmt::lit::refs_heads(default_branch).into(),
        )],
    )?;
+
    stored.set_remote_identity_root_to(pk, identity)?;
+
    stored.set_identity_head_to(identity)?;
+
    stored.set_head()?;

-
    let signed = project.sign_refs(signer)?;
-
    let _head = project.set_identity_head()?;
-
    let _head = project.set_head()?;
+
    let signed = stored.sign_refs(signer)?;

    Ok(signed)
}
modified radicle/src/storage.rs
@@ -355,8 +355,8 @@ impl Remote<Unverified> {
}

impl Remote<Unverified> {
-
    pub fn verified(self) -> Result<Remote<Verified>, crypto::Error> {
-
        let refs = self.refs.verified()?;
+
    pub fn verified<R: ReadRepository>(self, repo: &R) -> Result<Remote<Verified>, Error> {
+
        let refs = self.refs.verified(repo)?;

        Ok(Remote { refs })
    }
@@ -628,11 +628,24 @@ pub trait WriteRepository: ReadRepository + SignRepository {
    fn set_head(&self) -> Result<SetHead, RepositoryError>;
    /// Set the repository 'rad/id' to the canonical commit, agreed by quorum.
    fn set_identity_head(&self) -> Result<Oid, RepositoryError> {
-
        let head = self.canonical_identity_head()?;
+
        let head = self.canonical_identity_head().unwrap();
        self.set_identity_head_to(head)?;

        Ok(head)
    }
+
    /// Set the identity root reference to the canonical identity root commit.
+
    fn set_remote_identity_root(&self, remote: &RemoteId) -> Result<Oid, RepositoryError> {
+
        let root = self.identity_root()?;
+
        self.set_remote_identity_root_to(remote, root)?;
+

+
        Ok(root)
+
    }
+
    /// Set the identity root reference to the given commit.
+
    fn set_remote_identity_root_to(
+
        &self,
+
        remote: &RemoteId,
+
        root: Oid,
+
    ) -> Result<(), RepositoryError>;
    /// Set the repository 'rad/id' to the given commit.
    fn set_identity_head_to(&self, commit: Oid) -> Result<(), RepositoryError>;
    /// Set the user info of the Git repository.
@@ -644,7 +657,7 @@ pub trait WriteRepository: ReadRepository + SignRepository {
/// Allows signing refs.
pub trait SignRepository {
    /// Sign the repository's refs under the `refs/rad/sigrefs` branch.
-
    fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, Error>;
+
    fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, RepositoryError>;
}

impl<T, S> ReadStorage for T
modified radicle/src/storage/git.rs
@@ -20,15 +20,15 @@ use crate::node::SyncedAt;
use crate::storage::refs;
use crate::storage::refs::{Refs, SignedRefs, SignedRefsAt};
use crate::storage::{
-
    ReadRepository, ReadStorage, Remote, Remotes, RepositoryError, RepositoryInfo, SetHead,
-
    SignRepository, WriteRepository, WriteStorage,
+
    ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
+
    WriteRepository, WriteStorage,
};
use crate::{git, node};

pub use crate::git::{
    ext, raw, refname, refspec, Oid, PatternStr, Qualified, RefError, RefString, UserInfo,
};
-
pub use crate::storage::Error;
+
pub use crate::storage::{Error, RepositoryError};

use super::refs::RefsAt;
use super::{RemoteId, RemoteRepository, ValidateRepository};
@@ -773,9 +773,8 @@ impl ReadRepository for Repository {

    fn identity_root(&self) -> Result<Oid, RepositoryError> {
        let oid = self.backend.refname_to_id(CANONICAL_IDENTITY.as_str())?;
-
        let walk = self.revwalk(oid.into())?.collect::<Vec<_>>();
-
        let root = walk
-
            .into_iter()
+
        let root = self
+
            .revwalk(oid.into())?
            .last()
            .ok_or(RepositoryError::Doc(DocError::Missing))??;

@@ -783,14 +782,8 @@ impl ReadRepository for Repository {
    }

    fn identity_root_of(&self, remote: &RemoteId) -> Result<Oid, RepositoryError> {
-
        let oid = self.identity_head_of(remote)?;
-
        let walk = self.revwalk(oid)?.collect::<Vec<_>>();
-
        let root = walk
-
            .into_iter()
-
            .last()
-
            .ok_or(RepositoryError::Doc(DocError::Missing))??;
-

-
        Ok(root.into())
+
        self.reference_oid(remote, &git::refs::storage::IDENTITY_ROOT)
+
            .map_err(RepositoryError::from)
    }

    fn canonical_identity_head(&self) -> Result<Oid, RepositoryError> {
@@ -857,6 +850,19 @@ impl WriteRepository for Repository {
        Ok(())
    }

+
    fn set_remote_identity_root_to(
+
        &self,
+
        remote: &RemoteId,
+
        root: Oid,
+
    ) -> Result<(), RepositoryError> {
+
        let refname = git::refs::storage::id_root(remote);
+

+
        self.raw()
+
            .reference(refname.as_str(), *root, true, "set-id-root (radicle)")?;
+

+
        Ok(())
+
    }
+

    fn set_user(&self, info: &UserInfo) -> Result<(), Error> {
        let mut config = self.backend.config()?;
        config.set_str("user.name", &info.name())?;
@@ -870,20 +876,24 @@ impl WriteRepository for Repository {
}

impl SignRepository for Repository {
-
    fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, Error> {
+
    fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, RepositoryError> {
        let remote = signer.public_key();
+
        // Ensure the root reference is set, which is checked during sigref verification.
+
        if self.identity_root_of(remote).is_err() {
+
            self.set_remote_identity_root(remote)?;
+
        }
        let mut refs = self.references_of(remote)?;
        // Don't sign the `rad/sigrefs` ref itself, and don't sign invalid OIDs.
        refs.retain(|name, oid| {
            name.as_refstr() != refs::SIGREFS_BRANCH.as_ref() && !oid.is_zero()
        });
-
        let signed = refs.signed(signer)?;
-

+
        let signed = refs.signed(signer)?.verified(self)?;
        signed.save(self)?;

        Ok(signed)
    }
}
+

pub mod trailers {
    use std::str::FromStr;

@@ -946,7 +956,6 @@ mod tests {
    use crate::git;
    use crate::storage::refs::SIGREFS_BRANCH;
    use crate::storage::{ReadRepository, ReadStorage};
-
    use crate::test::arbitrary;
    use crate::test::fixtures;

    #[test]
@@ -999,7 +1008,13 @@ mod tests {

        assert_eq!(
            refs,
-
            vec![&cob, "refs/heads/master", "refs/rad/id", "refs/rad/sigrefs"]
+
            vec![
+
                &cob,
+
                "refs/heads/master",
+
                "refs/rad/id",
+
                "refs/rad/root",
+
                "refs/rad/sigrefs"
+
            ]
        );
    }

@@ -1009,28 +1024,26 @@ mod tests {
        let mut rng = fastrand::Rng::new();
        let signer = MockSigner::new(&mut rng);
        let storage = Storage::open(tmp.path(), fixtures::user()).unwrap();
-
        let proj_id = arbitrary::gen::<RepoId>(1);
        let alice = *signer.public_key();
-
        let project = storage.create(proj_id).unwrap();
-
        let backend = &project.backend;
+
        let (rid, _, working, _) =
+
            fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
+
        let stored = storage.repository(rid).unwrap();
        let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
-
        let head = git::initial_commit(backend, &sig).unwrap();
-
        let tree =
-
            git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), backend).unwrap();
+
        let head = working.head().unwrap().peel_to_commit().unwrap();

        git::commit(
-
            backend,
+
            &working,
            &head,
            &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
            "Second commit",
            &sig,
-
            &tree,
+
            &head.tree().unwrap(),
        )
        .unwrap();

-
        let signed = project.sign_refs(&signer).unwrap();
-
        let remote = project.remote(&alice).unwrap();
-
        let mut unsigned = project.references_of(&alice).unwrap();
+
        let signed = stored.sign_refs(&signer).unwrap();
+
        let remote = stored.remote(&alice).unwrap();
+
        let mut unsigned = stored.references_of(&alice).unwrap();

        // The signed refs doesn't contain the signature ref itself.
        let sigref = (*SIGREFS_BRANCH).to_ref_string();
modified radicle/src/storage/git/cob.rs
@@ -24,7 +24,7 @@ use crate::{

use super::{RemoteId, Repository};

-
pub use crate::cob::{store, ObjectId};
+
pub use crate::cob::{store, ObjectId, Store};

#[derive(Error, Debug)]
pub enum ObjectsError {
@@ -226,7 +226,7 @@ impl<'a, R: storage::ReadRepository> SignRepository for DraftStore<'a, R> {
    fn sign_refs<G: crypto::Signer>(
        &self,
        signer: &G,
-
    ) -> Result<storage::refs::SignedRefs<Verified>, Error> {
+
    ) -> Result<storage::refs::SignedRefs<Verified>, RepositoryError> {
        // Since this is a draft store, we do not actually want to sign the refs.
        // Instead, we just return the existing signed refs.
        let remote = self.repo.remote(signer.public_key())?;
modified radicle/src/storage/refs.rs
@@ -16,7 +16,7 @@ use crate::git::ext as git_ext;
use crate::git::Oid;
use crate::profile::env;
use crate::storage;
-
use crate::storage::{ReadRepository, RemoteId, WriteRepository};
+
use crate::storage::{ReadRepository, RemoteId, RepoId, WriteRepository};

pub use crate::git::refs::storage::*;

@@ -44,6 +44,12 @@ pub enum Error {
    Canonical(#[from] canonical::Error),
    #[error("invalid reference")]
    InvalidRef,
+
    #[error("missing identity root reference '{0}'")]
+
    MissingIdentityRoot(git::RefString),
+
    #[error("missing identity object '{0}'")]
+
    MissingIdentity(Oid),
+
    #[error("mismatched identity: local {local}, remote {remote}")]
+
    MismatchedIdentity { local: RepoId, remote: RepoId },
    #[error("invalid reference: {0}")]
    Ref(#[from] git::RefError),
    #[error(transparent)]
@@ -72,27 +78,17 @@ pub struct Refs(BTreeMap<git::RefString, Oid>);

impl Refs {
    /// Verify the given signature on these refs, and return [`SignedRefs`] on success.
-
    pub fn verified(
+
    pub fn verified<R: ReadRepository>(
        self,
        signer: PublicKey,
        signature: Signature,
+
        repo: &R,
    ) -> Result<SignedRefs<Verified>, Error> {
-
        let refs = self;
-
        let msg = refs.canonical();
-

-
        match signer.verify(msg, &signature) {
-
            Ok(()) => Ok(SignedRefs {
-
                refs,
-
                signature,
-
                id: signer,
-
                _verified: PhantomData,
-
            }),
-
            Err(e) => Err(e.into()),
-
        }
+
        SignedRefs::new(self, signer, signature).verified(repo)
    }

    /// Sign these refs with the given signer and return [`SignedRefs`].
-
    pub fn signed<G>(self, signer: &G) -> Result<SignedRefs<Verified>, Error>
+
    pub fn signed<G>(self, signer: &G) -> Result<SignedRefs<Unverified>, Error>
    where
        G: Signer,
    {
@@ -100,12 +96,7 @@ impl Refs {
        let msg = refs.canonical();
        let signature = signer.try_sign(&msg)?;

-
        Ok(SignedRefs {
-
            refs,
-
            signature,
-
            id: *signer.public_key(),
-
            _verified: PhantomData,
-
        })
+
        Ok(SignedRefs::new(refs, *signer.public_key(), signature))
    }

    /// Get a particular ref.
@@ -216,17 +207,17 @@ pub struct SignedRefs<V> {
}

impl SignedRefs<Unverified> {
-
    pub fn new(refs: Refs, id: PublicKey, signature: Signature) -> Self {
+
    pub fn new(refs: Refs, author: PublicKey, signature: Signature) -> Self {
        Self {
            refs,
            signature,
-
            id,
+
            id: author,
            _verified: PhantomData,
        }
    }

-
    pub fn verified(self) -> Result<SignedRefs<Verified>, crypto::Error> {
-
        match self.verify(&self.id) {
+
    pub fn verified<R: ReadRepository>(self, repo: &R) -> Result<SignedRefs<Verified>, Error> {
+
        match self.verify(repo) {
            Ok(()) => Ok(SignedRefs {
                refs: self.refs,
                signature: self.signature,
@@ -237,13 +228,36 @@ impl SignedRefs<Unverified> {
        }
    }

-
    pub fn verify(&self, signer: &PublicKey) -> Result<(), crypto::Error> {
+
    pub fn verify<R: ReadRepository>(&self, repo: &R) -> Result<(), Error> {
        let canonical = self.refs.canonical();
+
        let local = repo.id();

-
        match signer.verify(canonical, &self.signature) {
-
            Ok(()) => Ok(()),
-
            Err(e) => Err(e),
+
        // Verify signature.
+
        if let Err(e) = self.id.verify(canonical, &self.signature) {
+
            return Err(e.into());
        }
+
        // If the identity root was signed, verify it points to the right place.
+
        if let Some(id_root) = self.refs.get(&IDENTITY_ROOT) {
+
            // Get the identity at the given oid.
+
            let Ok(doc) = repo.identity_doc_at(id_root) else {
+
                return Err(Error::MissingIdentity(id_root));
+
            };
+
            let remote = RepoId::from(doc.blob);
+

+
            // Make sure the signed identity points to the local repo identity.
+
            if remote != local {
+
                return Err(Error::MismatchedIdentity { local, remote });
+
            }
+
        } else {
+
            // TODO(cloudhead): Make this into a hard error (`Error::MissingIdentityRoot`) for
+
            // repos that have migrated to the new identity document schema.
+
            log::debug!(
+
                target: "storage",
+
                "Signed ref verification for {} in {local}: {} is not provided",
+
                self.id, *IDENTITY_ROOT
+
            );
+
        }
+
        Ok(())
    }
}

@@ -264,20 +278,9 @@ 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 = signature.content().try_into()?;
+
        let refs = Refs::from_canonical(refs.content())?;

-
        match remote.verify(refs.content(), &signature) {
-
            Ok(()) => {
-
                let refs = Refs::from_canonical(refs.content())?;
-

-
                Ok(Self {
-
                    refs,
-
                    signature,
-
                    id: remote,
-
                    _verified: PhantomData,
-
                })
-
            }
-
            Err(e) => Err(e.into()),
-
        }
+
        SignedRefs::new(refs, remote, signature).verified(repo)
    }

    /// Save the signed refs to disk.
@@ -290,7 +293,7 @@ impl SignedRefs<Verified> {
        // N.b. if the signatures match then there are no updates
        let parent = match SignedRefsAt::load(*remote, repo)? {
            Some(SignedRefsAt { sigrefs, at }) if sigrefs.signature == self.signature => {
-
                return Ok(Updated::Unchanged { oid: at })
+
                return Ok(Updated::Unchanged { oid: at });
            }
            Some(SignedRefsAt { at, .. }) => Some(raw.find_commit(*at)?),
            None => None,
@@ -460,8 +463,13 @@ pub mod canonical {

#[cfg(test)]
mod tests {
-
    use super::*;
+
    use crypto::test::signer::MockSigner;
    use qcheck_macros::quickcheck;
+
    use storage::{git::transport, RemoteRepository, SignRepository, WriteStorage};
+

+
    use super::*;
+
    use crate::assert_matches;
+
    use crate::{cob::identity::Identity, rad, test::fixtures, Storage};

    #[quickcheck]
    fn prop_canonical_roundtrip(refs: Refs) {
@@ -470,4 +478,172 @@ mod tests {

        assert_eq!(refs, decoded);
    }
+

+
    #[test]
+
    // 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.
+
        assert_matches!(
+
            london.remote(bob.public_key()),
+
            Err(Error::MismatchedIdentity {
+
                local,
+
                remote,
+
            })
+
            if local == london_rid && remote == paris_rid
+
        );
+
    }
}
modified radicle/src/test/arbitrary.rs
@@ -71,12 +71,13 @@ pub fn vec<T: Eq + Arbitrary>(size: usize) -> Vec<T> {
pub fn nonempty_storage(size: usize) -> MockStorage {
    let mut storage = gen::<MockStorage>(size);
    for _ in 0..size {
-
        let id = gen::<RepoId>(1);
+
        let doc = gen::<DocAt>(1);
+
        let id = RepoId::from(doc.blob);
        storage.repos.insert(
            id,
            MockRepository {
                id,
-
                doc: gen::<DocAt>(1),
+
                doc,
                remotes: HashMap::new(),
            },
        );
@@ -90,9 +91,9 @@ pub fn gen<T: Arbitrary>(size: usize) -> T {
    T::arbitrary(&mut gen)
}

-
impl Arbitrary for storage::Remotes<crypto::Verified> {
+
impl Arbitrary for storage::Remotes<crypto::Unverified> {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
-
        let remotes: RandomMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
+
        let remotes: RandomMap<storage::RemoteId, storage::Remote<crypto::Unverified>> =
            Arbitrary::arbitrary(g);

        storage::Remotes::new(remotes)
@@ -181,10 +182,10 @@ impl Arbitrary for SignedRefs<Unverified> {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
        let bytes: [u8; 64] = Arbitrary::arbitrary(g);
        let signature = crypto::Signature::from(bytes);
-
        let id = PublicKey::arbitrary(g);
+
        let author = PublicKey::arbitrary(g);
        let refs = Refs::arbitrary(g);

-
        Self::new(refs, id, signature)
+
        Self::new(refs, author, signature)
    }
}

@@ -243,13 +244,13 @@ impl Arbitrary for MockRepository {
    }
}

-
impl Arbitrary for storage::Remote<crypto::Verified> {
+
impl Arbitrary for storage::Remote<crypto::Unverified> {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
        let refs = Refs::arbitrary(g);
        let signer = MockSigner::arbitrary(g);
        let signed = refs.signed(&signer).unwrap();

-
        storage::Remote::<crypto::Verified>::new(signed)
+
        storage::Remote::<crypto::Unverified>::new(signed)
    }
}

modified radicle/src/test/storage.rs
@@ -300,19 +300,19 @@ impl ReadRepository for MockRepository {
    }

    fn identity_head_of(&self, _remote: &RemoteId) -> Result<Oid, git::ext::Error> {
-
        todo!()
+
        Ok(self.doc.commit)
    }

    fn identity_root(&self) -> Result<Oid, RepositoryError> {
-
        todo!()
+
        Ok(self.doc.commit)
    }

    fn identity_root_of(&self, _remote: &RemoteId) -> Result<Oid, RepositoryError> {
-
        todo!()
+
        Ok(self.doc.commit)
    }

    fn canonical_identity_head(&self) -> Result<Oid, RepositoryError> {
-
        Ok(Oid::from_str("cccccccccccccccccccccccccccccccccccccccc").unwrap())
+
        Ok(self.doc.commit)
    }

    fn merge_base(&self, _left: &Oid, _right: &Oid) -> Result<Oid, git::ext::Error> {
@@ -333,6 +333,14 @@ impl WriteRepository for MockRepository {
        todo!()
    }

+
    fn set_remote_identity_root_to(
+
        &self,
+
        _remote: &RemoteId,
+
        _root: Oid,
+
    ) -> Result<(), RepositoryError> {
+
        todo!()
+
    }
+

    fn set_user(&self, _info: &git::UserInfo) -> Result<(), Error> {
        todo!()
    }
@@ -342,7 +350,7 @@ impl SignRepository for MockRepository {
    fn sign_refs<G: Signer>(
        &self,
        _signer: &G,
-
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
+
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, RepositoryError> {
        todo!()
    }
}