Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Implement signed refs
Alexis Sellier committed 3 years ago
commit cd7421eb86c010323bbb12ecc745f75946fc402d
parent 6e86218c3611d8e626b2e60b15ae4ea899a49855
14 files changed +777 -173
modified node/src/client.rs
@@ -55,7 +55,7 @@ pub struct Client<R: Reactor> {
}

impl<R: Reactor> Client<R> {
-
    pub fn new<P: AsRef<Path>, S: Signer>(
+
    pub fn new<P: AsRef<Path>, S: Signer + 'static>(
        path: P,
        signer: S,
    ) -> Result<Self, nakamoto_net::error::Error> {
modified node/src/crypto.rs
@@ -5,9 +5,17 @@ use ed25519_consensus as ed25519;
use serde::{Deserialize, Serialize};
use thiserror::Error;

+
pub use ed25519::Error;
pub use ed25519::Signature;

-
pub trait Signer: Send + Sync + 'static {
+
/// Verified (used as type witness).
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Verified;
+
/// Unverified (used as type witness).
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Unverified;
+

+
pub trait Signer: Send + Sync {
    /// Return this signer's public/verification key.
    fn public_key(&self) -> &PublicKey;
    /// Sign a message and return the signature.
@@ -27,6 +35,19 @@ where
    }
}

+
impl<T> Signer for &T
+
where
+
    T: Signer + ?Sized,
+
{
+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.deref().sign(msg)
+
    }
+

+
    fn public_key(&self) -> &PublicKey {
+
        self.deref().public_key()
+
    }
+
}
+

/// The public/verification key.
#[derive(Serialize, Deserialize, Eq, Debug, Copy, Clone)]
#[serde(transparent)]
modified node/src/git.rs
@@ -5,7 +5,8 @@ use radicle_git_ext as git_ext;

use crate::collections::HashMap;
use crate::identity::UserId;
-
use crate::storage::{Remote, Remotes, Unverified};
+
use crate::storage::refs::Refs;
+
use crate::storage::RemoteId;

pub use git_ext::Oid;
pub use git_url::Url;
@@ -30,7 +31,7 @@ pub enum ListRefsError {
}

/// List remote refs of a project, given the remote URL.
-
pub fn list_remotes(url: &Url) -> Result<Remotes<Unverified>, ListRefsError> {
+
pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError> {
    let url = url.to_string();
    let mut remotes = HashMap::default();
    let mut remote = git2::Remote::create_detached(&url)?;
@@ -40,14 +41,12 @@ pub fn list_remotes(url: &Url) -> Result<Remotes<Unverified>, ListRefsError> {
    let refs = remote.list()?;
    for r in refs {
        let (id, refname) = parse_ref::<UserId>(r.name())?;
-
        let entry = remotes
-
            .entry(id)
-
            .or_insert_with(|| Remote::new(id, HashMap::default()));
+
        let entry = remotes.entry(id).or_insert_with(Refs::default);

-
        entry.refs.insert(refname.to_string(), r.oid().into());
+
        entry.insert(refname.to_string(), r.oid().into());
    }

-
    Ok(Remotes::new(remotes))
+
    Ok(remotes)
}

/// Parse a ref string.
@@ -90,6 +89,7 @@ pub fn commit<'a>(
    let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?;
    let tree_id = repo.index()?.write_tree()?;
    let tree = repo.find_tree(tree_id)?;
+
    // TODO: Take the ref as parameter.
    let oid = repo.commit(None, &sig, &sig, message, &tree, &[parent])?;
    let commit = repo.find_commit(oid).unwrap();

modified node/src/protocol.rs
@@ -24,7 +24,7 @@ use crate::protocol::config::ProjectTracking;
use crate::protocol::message::Message;
use crate::protocol::peer::{Peer, PeerError, PeerState};
use crate::storage::{self, ReadRepository, WriteRepository};
-
use crate::storage::{Inventory, ReadStorage, Remotes, Unverified, WriteStorage};
+
use crate::storage::{Inventory, ReadStorage, Remotes, WriteStorage};

pub use crate::protocol::config::{Config, Network};

@@ -339,21 +339,11 @@ where
                let user = *self.storage.user_id();
                let repo = self.storage.repository(&proj).unwrap();
                let remote = repo.remote(&user).unwrap();
-
                let refs = remote.refs;
-
                let signature = self
-
                    .signer
-
                    .sign(serde_json::to_vec(&refs).unwrap().as_slice());
                let peers = self.peers.negotiated().map(|(_, p)| p.addr);
+
                let refs = remote.refs.unverified();

-
                self.context.broadcast(
-
                    Message::RefsUpdate {
-
                        proj,
-
                        user,
-
                        refs,
-
                        signature,
-
                    },
-
                    peers,
-
                );
+
                self.context
+
                    .broadcast(Message::RefsUpdate { proj, user, refs }, peers);
            }
        }
    }
@@ -556,7 +546,7 @@ impl<S, T, G> Iterator for Protocol<S, T, G> {
#[derive(Debug)]
pub struct Lookup {
    /// Whether the project was found locally or not.
-
    pub local: Option<Remotes<Unverified>>,
+
    pub local: Option<Remotes<crypto::Verified>>,
    /// A list of remote peers on which the project is known to exist.
    pub remote: Vec<NodeId>,
}
modified node/src/protocol/message.rs
@@ -7,6 +7,7 @@ use crate::crypto;
use crate::identity::{ProjId, UserId};
use crate::protocol::{Context, NodeId, Timestamp, PROTOCOL_VERSION};
use crate::storage;
+
use crate::storage::refs::SignedRefs;

/// Message envelope. All messages sent over the network are wrapped in this type.
#[derive(Debug, Serialize, Deserialize)]
@@ -104,9 +105,7 @@ pub enum Message {
        /// User signing.
        user: UserId,
        /// Updated refs.
-
        refs: storage::Refs,
-
        /// Signature over the refs.
-
        signature: crypto::Signature,
+
        refs: SignedRefs<crypto::Unverified>,
    },
}

modified node/src/protocol/peer.rs
@@ -194,18 +194,8 @@ impl Peer {
                    }));
                }
            }
-
            (
-
                PeerState::Negotiated { git, .. },
-
                Message::RefsUpdate {
-
                    proj,
-
                    user,
-
                    refs,
-
                    signature,
-
                },
-
            ) => {
-
                let bytes = serde_json::to_vec(&refs).unwrap();
-

-
                if user.verify(&signature, &bytes).is_ok() {
+
            (PeerState::Negotiated { git, .. }, Message::RefsUpdate { proj, user, refs }) => {
+
                if refs.verified(&user).is_ok() {
                    // TODO: Buffer/throttle fetches.
                    // TODO: Also pass the updated refs so that we can check whether
                    // we need to fetch or not.
modified node/src/rad.rs
@@ -4,9 +4,11 @@ use git_url::Url;
use nonempty::NonEmpty;
use thiserror::Error;

+
use crate::crypto::Verified;
use crate::git;
use crate::identity::ProjId;
use crate::storage::git::RADICLE_ID_REF;
+
use crate::storage::refs::SignedRefs;
use crate::storage::{BranchName, ReadRepository as _};
use crate::{identity, storage};

@@ -37,7 +39,7 @@ pub fn init<S: storage::WriteStorage>(
    description: &str,
    default_branch: BranchName,
    storage: S,
-
) -> Result<ProjId, InitError> {
+
) -> Result<(ProjId, SignedRefs<Verified>), InitError> {
    let user_id = storage.user_id();
    let delegate = identity::Delegate {
        // TODO: Use actual user name.
@@ -114,8 +116,9 @@ pub fn init<S: storage::WriteStorage>(
        ],
        None,
    )?;
+
    let signed = storage.sign_refs(&project)?;

-
    Ok(id)
+
    Ok((id, signed))
}

#[cfg(test)]
@@ -136,7 +139,7 @@ mod tests {
        let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap();
        let _branch = repo.branch("master", &head, false).unwrap();

-
        init(
+
        let (_id, _refs) = init(
            &repo,
            "acme",
            "Acme's repo",
modified node/src/storage.rs
@@ -1,4 +1,5 @@
pub mod git;
+
pub mod refs;

use std::collections::hash_map;
use std::io;
@@ -6,19 +7,21 @@ use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::path::Path;

-
use git_url::Url;
-
use once_cell::sync::Lazy;
+
use radicle_git_ext as git_ext;
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub use radicle_git_ext::Oid;

use crate::collections::HashMap;
+
use crate::crypto::{self, Unverified, Verified};
use crate::git::RefError;
+
use crate::git::Url;
use crate::identity;
use crate::identity::{ProjId, ProjIdError, UserId};
+
use crate::storage::refs::Refs;

-
pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new(".rad/identity.toml"));
+
use self::refs::SignedRefs;

pub type BranchName = String;
pub type Inventory = Vec<ProjId>;
@@ -30,6 +33,8 @@ pub enum Error {
    InvalidRef,
    #[error("git reference error: {0}")]
    Ref(#[from] RefError),
+
    #[error(transparent)]
+
    Refs(#[from] refs::Error),
    #[error("git: {0}")]
    Git(#[from] git2::Error),
    #[error("id: {0}")]
@@ -42,28 +47,38 @@ pub enum Error {
    InvalidHead,
}

-
pub type Refs = HashMap<RefName, Oid>;
pub type RemoteId = UserId;
pub type RefName = String;

-
/// Verified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct Verified;
-
/// Unverified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct Unverified;
-

/// Project remotes. Tracks the git state of a project.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);

-
impl Remotes<Unverified> {
-
    pub fn new(remotes: HashMap<RemoteId, Remote<Unverified>>) -> Self {
+
impl<V> Deref for Remotes<V> {
+
    type Target = HashMap<RemoteId, Remote<V>>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl<V> Remotes<V> {
+
    pub fn new(remotes: HashMap<RemoteId, Remote<V>>) -> Self {
        Self(remotes)
    }
}

-
impl Default for Remotes<Unverified> {
+
impl Remotes<Verified> {
+
    pub fn unverified(self) -> Remotes<Unverified> {
+
        Remotes(
+
            self.into_iter()
+
                .map(|(id, r)| (id, r.unverified()))
+
                .collect(),
+
        )
+
    }
+
}
+

+
impl<V> Default for Remotes<V> {
    fn default() -> Self {
        Self(HashMap::default())
    }
@@ -90,13 +105,25 @@ impl Into<HashMap<String, Remote<Unverified>>> for Remotes<Unverified> {
    }
}

+
#[allow(clippy::from_over_into)]
+
impl<V> Into<HashMap<RemoteId, Refs>> for Remotes<V> {
+
    fn into(self) -> HashMap<RemoteId, Refs> {
+
        let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into());
+

+
        for (k, v) in self.into_iter() {
+
            remotes.insert(k, v.refs.into());
+
        }
+
        remotes
+
    }
+
}
+

/// A project remote.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Remote<V> {
    /// ID of remote.
    pub id: UserId,
    /// Git references published under this remote, and their hashes.
-
    pub refs: HashMap<RefName, Oid>,
+
    pub refs: SignedRefs<V>,
    /// Whether this remote is of a project delegate.
    pub delegate: bool,
    /// Whether the remote is verified or not, ie. whether its signed refs were checked.
@@ -104,21 +131,45 @@ pub struct Remote<V> {
    verified: PhantomData<V>,
}

-
impl Remote<Unverified> {
-
    pub fn new(id: UserId, refs: HashMap<RefName, Oid>) -> Self {
+
impl<V> Remote<V> {
+
    pub fn new(id: UserId, refs: impl Into<SignedRefs<V>>) -> Self {
        Self {
            id,
-
            refs,
+
            refs: refs.into(),
            delegate: false,
            verified: PhantomData,
        }
    }
}

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

+
        Ok(Remote {
+
            id: self.id,
+
            refs,
+
            delegate: self.delegate,
+
            verified: PhantomData,
+
        })
+
    }
+
}
+

+
impl Remote<Verified> {
+
    pub fn unverified(self) -> Remote<Unverified> {
+
        Remote {
+
            id: self.id,
+
            refs: self.refs.unverified(),
+
            delegate: self.delegate,
+
            verified: PhantomData,
+
        }
+
    }
+
}
+

pub trait ReadStorage {
    fn user_id(&self) -> &UserId;
    fn url(&self) -> Url;
-
    fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error>;
+
    fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error>;
    fn inventory(&self) -> Result<Inventory, Error>;
}

@@ -126,16 +177,26 @@ pub trait WriteStorage: ReadStorage {
    type Repository: WriteRepository;

    fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error>;
+
    fn sign_refs(&self, repository: &Self::Repository) -> Result<SignedRefs<Verified>, Error>;
}

pub trait ReadRepository {
    fn path(&self) -> &Path;
-
    fn remote(&self, user: &UserId) -> Result<Remote<Unverified>, Error>;
-
    fn remotes(&self) -> Result<Remotes<Unverified>, Error>;
+
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
+
    fn reference(
+
        &self,
+
        user: &UserId,
+
        reference: &str,
+
    ) -> Result<Option<git2::Reference>, git2::Error>;
+
    fn reference_oid(&self, user: &UserId, reference: &str) -> Result<Option<Oid>, git2::Error>;
+
    fn references(&self, user: &UserId) -> Result<Refs, Error>;
+
    fn remote(&self, user: &UserId) -> Result<Remote<Verified>, refs::Error>;
+
    fn remotes(&self) -> Result<Remotes<Verified>, refs::Error>;
}

pub trait WriteRepository: ReadRepository {
    fn fetch(&mut self, url: &Url) -> Result<(), git2::Error>;
+
    fn raw(&self) -> &git2::Repository;
}

impl<T, S> ReadStorage for T
@@ -155,7 +216,7 @@ where
        self.deref().inventory()
    }

-
    fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
+
    fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
        self.deref().get(proj)
    }
}
@@ -170,6 +231,10 @@ where
    fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error> {
        self.deref().repository(proj)
    }
+

+
    fn sign_refs(&self, repository: &S::Repository) -> Result<SignedRefs<Verified>, Error> {
+
        self.deref().sign_refs(repository)
+
    }
}

#[cfg(test)]
modified node/src/storage/git.rs
@@ -1,28 +1,32 @@
+
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{fmt, fs, io};

use git_ref_format::refspec;
-
use git_url::Url;
use once_cell::sync::Lazy;
use radicle_git_ext as git_ext;

pub use radicle_git_ext::Oid;

use crate::collections::HashMap;
-
use crate::crypto::Signer;
+
use crate::crypto::{Signer, Verified};
use crate::git;
use crate::identity::{ProjId, UserId};
-

-
use super::{
-
    Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, Unverified, WriteRepository,
-
    WriteStorage,
+
use crate::storage::refs;
+
use crate::storage::refs::{Refs, SignedRefs};
+
use crate::storage::{
+
    Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, WriteRepository, WriteStorage,
};

+
use super::RemoteId;
+

pub static RADICLE_ID_REF: Lazy<refspec::PatternString> =
    Lazy::new(|| refspec::pattern!("heads/radicle/id"));
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
    Lazy::new(|| refspec::pattern!("refs/remotes/*"));
+
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
+
    Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));

pub struct Storage {
    path: PathBuf,
@@ -40,16 +44,28 @@ impl ReadStorage for Storage {
        self.signer.public_key()
    }

-
    fn url(&self) -> Url {
-
        Url {
+
    fn url(&self) -> git::Url {
+
        git::Url {
            scheme: git_url::Scheme::File,
            host: Some(self.path.to_string_lossy().to_string()),
-
            ..Url::default()
+
            ..git::Url::default()
        }
    }

-
    fn get(&self, _id: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
-
        todo!()
+
    fn get(&self, id: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
+
        // TODO: Don't create a repo here if it doesn't exist?
+
        //       Perhaps for checking we could have a `contains` method?
+
        match self.repository(id) {
+
            Ok(r) => {
+
                let remotes = r.remotes()?;
+
                if remotes.is_empty() {
+
                    Ok(None)
+
                } else {
+
                    Ok(Some(remotes))
+
                }
+
            }
+
            Err(e) => Err(e),
+
        }
    }

    fn inventory(&self) -> Result<Inventory, Error> {
@@ -63,10 +79,23 @@ impl WriteStorage for Storage {
    fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error> {
        Repository::open(self.path.join(proj.to_string()))
    }
+

+
    fn sign_refs(&self, repository: &Repository) -> Result<SignedRefs<Verified>, Error> {
+
        let remote = self.signer.public_key();
+
        let refs = repository.references(remote)?;
+
        let signed = refs.signed(self.signer.clone())?;
+

+
        signed.save(remote, repository)?;
+

+
        Ok(signed)
+
    }
}

impl Storage {
-
    pub fn open<P: AsRef<Path>>(path: P, signer: impl Signer) -> Result<Self, io::Error> {
+
    pub fn open<P: AsRef<Path>, S: Signer + 'static>(
+
        path: P,
+
        signer: S,
+
    ) -> Result<Self, io::Error> {
        let path = path.as_ref().to_path_buf();

        match fs::create_dir_all(&path) {
@@ -89,6 +118,13 @@ impl Storage {
        self.signer.clone()
    }

+
    pub fn with_signer(&self, signer: impl Signer + 'static) -> Self {
+
        Self {
+
            path: self.path.clone(),
+
            signer: Arc::new(signer),
+
        }
+
    }
+

    pub fn projects(&self) -> Result<Vec<ProjId>, Error> {
        let mut projects = Vec::new();

@@ -100,10 +136,28 @@ impl Storage {
        }
        Ok(projects)
    }
+

+
    pub fn inspect(&self) -> Result<(), Error> {
+
        for proj in self.projects()? {
+
            let repo = self.repository(&proj)?;
+

+
            for r in repo.raw().references()? {
+
                let r = r?;
+
                let name = r.name().ok_or(Error::InvalidRef)?;
+
                let oid = r.target().ok_or(Error::InvalidRef)?;
+

+
                println!("{} {} {}", proj, oid, name);
+
            }
+
        }
+
        Ok(())
+
    }
}

pub struct Repository {
    pub(crate) backend: git2::Repository,
+
    // TODO: Add project id here so we can refer to it
+
    // in a bunch of places. We could write it to the
+
    // git config for later.
}

impl Repository {
@@ -117,6 +171,11 @@ impl Repository {
                        .no_reinit(true)
                        .external_template(false),
                )?;
+
                let mut config = backend.config()?;
+

+
                // TODO: Get ahold of user name and/or key.
+
                config.set_str("user.name", "radicle")?;
+
                config.set_str("user.email", "radicle@localhost")?;

                Ok(backend)
            }
@@ -127,15 +186,15 @@ impl Repository {
        Ok(Self { backend })
    }

-
    pub fn find_reference(&self, remote: &UserId, name: &str) -> Result<Oid, Error> {
-
        let name = format!("refs/remotes/{}/{}", remote, name);
-
        let target = self
-
            .backend
-
            .find_reference(&name)?
-
            .target()
-
            .ok_or(Error::InvalidRef)?;
+
    pub fn inspect(&self) -> Result<(), Error> {
+
        for r in self.backend.references()? {
+
            let r = r?;
+
            let name = r.name().ok_or(Error::InvalidRef)?;
+
            let oid = r.target().ok_or(Error::InvalidRef)?;

-
        Ok(target.into())
+
            println!("{} {}", oid, name);
+
        }
+
        Ok(())
    }
}

@@ -144,38 +203,69 @@ impl ReadRepository for Repository {
        self.backend.path()
    }

-
    fn remote(&self, user: &UserId) -> Result<Remote<Unverified>, Error> {
-
        // TODO: Only fetch standard refs.
+
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error> {
+
        git_ext::Blob::At {
+
            object: oid.into(),
+
            path,
+
        }
+
        .get(&self.backend)
+
    }
+

+
    fn reference(
+
        &self,
+
        remote: &RemoteId,
+
        name: &str,
+
    ) -> Result<Option<git2::Reference>, git2::Error> {
+
        let name = format!("refs/remotes/{remote}/{name}");
+
        self.backend.find_reference(&name).map(Some).or_else(|e| {
+
            if git_ext::is_not_found_err(&e) {
+
                Ok(None)
+
            } else {
+
                Err(e)
+
            }
+
        })
+
    }
+

+
    fn reference_oid(&self, user: &RemoteId, reference: &str) -> Result<Option<Oid>, git2::Error> {
+
        let reference = self.reference(user, reference)?;
+
        Ok(reference.and_then(|r| r.target().map(|o| o.into())))
+
    }
+

+
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
+
        let refs = SignedRefs::load(remote, self)?;
+
        Ok(Remote::new(*remote, refs))
+
    }
+

+
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error> {
+
        // TODO: Only return known refs, eg. heads/ rad/ tags/ etc..
        let entries = self
            .backend
-
            .references_glob(format!("refs/remotes/{}/*", user).as_str())?;
-
        let mut refs = HashMap::default();
+
            .references_glob(format!("refs/remotes/{remote}/*").as_str())?;
+
        let mut refs = BTreeMap::new();

        for e in entries {
            let e = e?;
            let name = e.name().ok_or(Error::InvalidRef)?;
-
            let (_, refname) = git::parse_ref::<UserId>(name)?;
+
            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
            let oid = e.target().ok_or(Error::InvalidRef)?;

            refs.insert(refname.to_string(), oid.into());
        }
-
        Ok(Remote::new(*user, refs))
+
        Ok(refs.into())
    }

-
    fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
-
        let refs = self.backend.references_glob(REMOTES_GLOB.as_str())?;
+
    fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
+
        // TODO: Should use the id glob here instead of signature.
+
        let refs = self.backend.references_glob(SIGNATURES_GLOB.as_str())?;
        let mut remotes = HashMap::default();

        for r in refs {
            let r = r?;
-
            let name = r.name().ok_or(Error::InvalidRef)?;
-
            let (id, refname) = git::parse_ref::<UserId>(name)?;
-
            let entry = remotes
-
                .entry(id)
-
                .or_insert_with(|| Remote::new(id, HashMap::default()));
-
            let oid = r.target().ok_or(Error::InvalidRef)?;
+
            let name = r.name().ok_or(refs::Error::InvalidRef)?;
+
            let (id, _) = git::parse_ref::<RemoteId>(name)?;
+
            let remote = self.remote(&id)?;

-
            entry.refs.insert(refname.to_string(), oid.into());
+
            remotes.insert(id, remote);
        }
        Ok(Remotes::new(remotes))
    }
@@ -183,7 +273,7 @@ impl ReadRepository for Repository {

impl WriteRepository for Repository {
    /// Fetch all remotes of a project from the given URL.
-
    fn fetch(&mut self, url: &Url) -> Result<(), git2::Error> {
+
    fn fetch(&mut self, url: &git::Url) -> Result<(), git2::Error> {
        // TODO: Have function to fetch specific remotes.
        // TODO: Return meaningful info on success.
        //
@@ -207,6 +297,10 @@ impl WriteRepository for Repository {

        Ok(())
    }
+

+
    fn raw(&self) -> &git2::Repository {
+
        &self.backend
+
    }
}

impl From<git2::Repository> for Repository {
@@ -219,28 +313,34 @@ impl From<git2::Repository> for Repository {
mod tests {
    use super::*;
    use crate::git;
+
    use crate::storage::refs::SIGNATURE_REF;
    use crate::storage::{ReadStorage, WriteRepository};
+
    use crate::test::arbitrary;
    use crate::test::crypto::MockSigner;
    use crate::test::fixtures;
-
    use git_url::Url;

    #[test]
-
    fn test_list_remotes() {
+
    fn test_remote_refs() {
        let dir = tempfile::tempdir().unwrap();
        let storage = fixtures::storage(dir.path());
        let inv = storage.inventory().unwrap();
        let proj = inv.first().unwrap();
-
        let refs = git::list_remotes(&Url {
+
        let mut refs = git::remote_refs(&git::Url {
            host: Some(dir.path().to_string_lossy().to_string()),
            scheme: git_url::Scheme::File,
            path: format!("/{}", proj).into(),
-
            ..Url::default()
+
            ..git::Url::default()
        })
        .unwrap();

-
        let remotes = storage.repository(proj).unwrap().remotes().unwrap();
+
        let project = storage.repository(proj).unwrap();
+
        let remotes = project.remotes().unwrap();

-
        assert_eq!(refs, remotes);
+
        // Strip the remote refs of sigrefs so we can compare them.
+
        for remote in refs.values_mut() {
+
            remote.remove(SIGNATURE_REF).unwrap();
+
        }
+
        assert_eq!(refs, remotes.into());
    }

    #[test]
@@ -256,27 +356,56 @@ mod tests {
        // Have Bob fetch Alice's refs.
        bob.repository(proj)
            .unwrap()
-
            .fetch(&Url {
+
            .fetch(&git::Url {
                host: Some(alice.path().to_string_lossy().to_string()),
                scheme: git_url::Scheme::File,
                path: format!("/{}", proj).into(),
-
                ..Url::default()
+
                ..git::Url::default()
            })
            .unwrap();

        for (id, _) in remotes.into_iter() {
-
            let alice_oid = alice
-
                .repository(proj)
-
                .unwrap()
-
                .find_reference(&id, refname)
-
                .unwrap();
-
            let bob_oid = bob
-
                .repository(proj)
-
                .unwrap()
-
                .find_reference(&id, refname)
-
                .unwrap();
-

-
            assert_eq!(alice_oid, bob_oid);
+
            let alice_repo = alice.repository(proj).unwrap();
+
            let alice_oid = alice_repo.reference(&id, refname).unwrap().unwrap();
+

+
            let bob_repo = bob.repository(proj).unwrap();
+
            let bob_oid = bob_repo.reference(&id, refname).unwrap().unwrap();
+

+
            assert_eq!(alice_oid.target(), bob_oid.target());
        }
    }
+

+
    #[test]
+
    fn test_sign_refs() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let mut rng = fastrand::Rng::new();
+
        let signer = MockSigner::new(&mut rng);
+
        let storage = Storage::open(tmp.path(), signer).unwrap();
+
        let proj_id = arbitrary::gen::<ProjId>(1);
+
        let alice = *storage.user_id();
+
        let project = storage.repository(&proj_id).unwrap();
+
        let backend = &project.backend;
+
        let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
+
        let head = git::initial_commit(backend, &sig).unwrap();
+

+
        let head = git::commit(backend, &head, "Second commit", &alice.to_string()).unwrap();
+
        backend
+
            .reference(
+
                &format!("refs/remotes/{alice}/heads/master"),
+
                head.id(),
+
                false,
+
                "test",
+
            )
+
            .unwrap();
+

+
        let signed = storage.sign_refs(&project).unwrap();
+
        let remote = project.remote(&alice).unwrap();
+
        let mut unsigned = project.references(&alice).unwrap();
+

+
        // The signed refs doesn't contain the signature ref itself.
+
        unsigned.remove(SIGNATURE_REF).unwrap();
+

+
        assert_eq!(remote.refs, signed);
+
        assert_eq!(*remote.refs, unsigned);
+
    }
}
added node/src/storage/refs.rs
@@ -0,0 +1,318 @@
+
use std::collections::BTreeMap;
+
use std::fmt::Debug;
+
use std::io;
+
use std::io::{BufRead, BufReader};
+
use std::marker::PhantomData;
+
use std::ops::{Deref, DerefMut};
+
use std::path::Path;
+
use std::str::FromStr;
+

+
use radicle_git_ext as git_ext;
+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::crypto;
+
use crate::crypto::{PublicKey, Signature, Signer, Unverified, Verified};
+
use crate::git;
+
use crate::git::Oid;
+
use crate::storage;
+
use crate::storage::{ReadRepository, RemoteId, WriteRepository};
+

+
pub const SIGNATURE_REF: &str = "radicle/signature";
+
pub const REFS_BLOB_PATH: &str = "refs";
+
pub const SIGNATURE_BLOB_PATH: &str = "signature";
+

+
#[derive(Debug)]
+
pub enum Updated {
+
    /// The computed [`Refs`] were stored as a new commit.
+
    Updated { oid: Oid },
+
    /// The stored [`Refs`] were the same as the computed ones, so no new commit
+
    /// was created.
+
    Unchanged { oid: Oid },
+
}
+

+
#[derive(Debug, Error)]
+
pub enum Error {
+
    #[error("invalid signature: {0}")]
+
    InvalidSignature(#[from] crypto::Error),
+
    #[error("canonical refs: {0}")]
+
    Canonical(#[from] canonical::Error),
+
    #[error("invalid reference")]
+
    InvalidRef,
+
    #[error("invalid reference: {0}")]
+
    Ref(#[from] git::RefError),
+
    #[error(transparent)]
+
    Git(#[from] git2::Error),
+
    #[error(transparent)]
+
    GitExt(#[from] git_ext::Error),
+
    #[error("refs were not found")]
+
    NotFound,
+
}
+

+
/// The published state of a local repository.
+
#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Refs(BTreeMap<String, Oid>);
+

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

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

+
    /// Sign these refs with the given signer and return [`SignedRefs`].
+
    pub fn signed<S>(self, signer: S) -> Result<SignedRefs<Verified>, Error>
+
    where
+
        S: Signer,
+
    {
+
        let refs = self;
+
        let msg = refs.canonical();
+
        let signature = signer.sign(&msg);
+

+
        Ok(SignedRefs {
+
            refs,
+
            signature,
+
            _verified: PhantomData,
+
        })
+
    }
+

+
    /// Create refs from a canonical representation.
+
    pub fn from_canonical(bytes: &[u8]) -> Result<Self, canonical::Error> {
+
        let reader = BufReader::new(bytes);
+
        let mut refs = BTreeMap::new();
+

+
        for line in reader.lines() {
+
            let line = line?;
+
            let (oid, name) = line
+
                .split_once(' ')
+
                .ok_or(canonical::Error::InvalidFormat)?;
+

+
            let name = name.to_owned();
+
            let oid = Oid::from_str(oid)?;
+

+
            refs.insert(name, oid);
+
        }
+
        Ok(Self(refs))
+
    }
+

+
    pub fn canonical(&self) -> Vec<u8> {
+
        let mut buf = String::new();
+
        let refs = self.iter().filter(|(name, _)| *name != SIGNATURE_REF);
+

+
        for (name, oid) in refs {
+
            buf.push_str(&oid.to_string());
+
            buf.push(' ');
+
            buf.push_str(name);
+
            buf.push('\n');
+
        }
+
        buf.into_bytes()
+
    }
+
}
+

+
impl From<Refs> for BTreeMap<String, Oid> {
+
    fn from(refs: Refs) -> Self {
+
        refs.0
+
    }
+
}
+

+
impl<V> From<SignedRefs<V>> for Refs {
+
    fn from(signed: SignedRefs<V>) -> Self {
+
        signed.refs
+
    }
+
}
+

+
impl From<BTreeMap<String, Oid>> for Refs {
+
    fn from(refs: BTreeMap<String, Oid>) -> Self {
+
        Self(refs)
+
    }
+
}
+

+
impl Deref for Refs {
+
    type Target = BTreeMap<String, Oid>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl DerefMut for Refs {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.0
+
    }
+
}
+

+
/// Combination of [`Refs`] and a [`Signature`]. The signature is a cryptographic
+
/// signature over the refs. This allows us to easily verify if a set of refs
+
/// came from a particular user.
+
///
+
/// The type parameter keeps track of whether the signature was [`Verified`] or
+
/// [`Unverified`].
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+
pub struct SignedRefs<V> {
+
    refs: Refs,
+
    signature: Signature,
+
    #[serde(skip)]
+
    _verified: PhantomData<V>,
+
}
+

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

+
    pub fn verified(self, signer: &PublicKey) -> Result<SignedRefs<Verified>, crypto::Error> {
+
        let canonical = self.refs.canonical();
+

+
        match signer.verify(&self.signature, &canonical) {
+
            Ok(()) => Ok(SignedRefs {
+
                refs: self.refs,
+
                signature: self.signature,
+
                _verified: PhantomData,
+
            }),
+
            Err(e) => Err(e),
+
        }
+
    }
+
}
+

+
impl SignedRefs<Verified> {
+
    pub fn load<S>(remote: &RemoteId, repo: &S) -> Result<Self, Error>
+
    where
+
        S: ReadRepository,
+
    {
+
        if let Some(oid) = repo.reference_oid(remote, SIGNATURE_REF)? {
+
            Self::load_at(oid, remote, repo)
+
        } else {
+
            Err(Error::NotFound)
+
        }
+
    }
+

+
    pub fn load_at<S>(oid: Oid, remote: &RemoteId, repo: &S) -> Result<Self, Error>
+
    where
+
        S: storage::ReadRepository,
+
    {
+
        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::try_from(signature.content())?;
+

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

+
                Ok(Self {
+
                    refs,
+
                    signature,
+
                    _verified: PhantomData,
+
                })
+
            }
+
            Err(e) => Err(e.into()),
+
        }
+
    }
+

+
    /// Save the signed refs to disk.
+
    /// This creates a new commit on the signed refs branch, and updates the branch pointer.
+
    pub fn save<S: WriteRepository>(
+
        &self,
+
        // TODO: This should be part of the signed refs.
+
        remote: &RemoteId,
+
        repo: &S,
+
    ) -> Result<Updated, Error> {
+
        let parent: Option<git2::Commit> = repo
+
            .reference(remote, SIGNATURE_REF)?
+
            .map(|r| r.peel_to_commit())
+
            .transpose()?;
+

+
        let tree = {
+
            let raw = repo.raw();
+
            let refs_blob_oid = raw.blob(&self.canonical())?;
+
            let sig_blob_oid = raw.blob(&self.signature.to_bytes())?;
+

+
            let mut builder = raw.treebuilder(None)?;
+
            builder.insert(REFS_BLOB_PATH, refs_blob_oid, 0o100_644)?;
+
            builder.insert(SIGNATURE_BLOB_PATH, sig_blob_oid, 0o100_644)?;
+

+
            let oid = builder.write()?;
+

+
            raw.find_tree(oid)
+
        }?;
+

+
        if let Some(ref parent) = parent {
+
            if parent.tree()?.id() == tree.id() {
+
                return Ok(Updated::Unchanged {
+
                    oid: parent.id().into(),
+
                });
+
            }
+
        }
+

+
        let sigref = format!("refs/remotes/{remote}/{SIGNATURE_REF}");
+
        let author = repo.raw().signature()?;
+
        let commit = repo.raw().commit(
+
            Some(&sigref),
+
            &author,
+
            &author,
+
            &format!("Update {} for {}", SIGNATURE_REF, remote),
+
            &tree,
+
            &parent.iter().collect::<Vec<&git2::Commit>>(),
+
        );
+

+
        match commit {
+
            Ok(oid) => Ok(Updated::Updated { oid: oid.into() }),
+
            Err(e) => match (e.class(), e.code()) {
+
                (git2::ErrorClass::Object, git2::ErrorCode::Modified) => {
+
                    log::warn!("Concurrent modification of refs: {:?}", e);
+

+
                    Err(Error::Git(e))
+
                }
+
                _ => Err(e.into()),
+
            },
+
        }
+
    }
+

+
    pub fn unverified(self) -> SignedRefs<Unverified> {
+
        SignedRefs {
+
            refs: self.refs,
+
            signature: self.signature,
+
            _verified: PhantomData,
+
        }
+
    }
+
}
+

+
impl<V> Deref for SignedRefs<V> {
+
    type Target = Refs;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.refs
+
    }
+
}
+

+
pub mod canonical {
+
    use super::*;
+

+
    #[derive(Debug, thiserror::Error)]
+
    pub enum Error {
+
        #[error("invalid canonical format")]
+
        InvalidFormat,
+
        #[error(transparent)]
+
        Io(#[from] io::Error),
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
    }
+
}
+

+
// TODO: Test canonical/from_canonical
modified node/src/test/arbitrary.rs
@@ -1,15 +1,21 @@
-
use std::collections::HashSet;
+
use std::collections::{BTreeMap, HashSet};
use std::hash::Hash;
use std::ops::RangeBounds;

+
use quickcheck::Arbitrary;
+

use crate::collections::HashMap;
-
use crate::crypto::PublicKey;
+
use crate::crypto::{self, Signer};
+
use crate::crypto::{PublicKey, SecretKey};
use crate::hash;
-
use crate::identity::{ProjId, UserId};
+
use crate::identity::ProjId;
use crate::storage;
+
use crate::storage::refs::Refs;
use crate::test::storage::MockStorage;

-
pub fn set<T: Eq + Hash + quickcheck::Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
+
use super::crypto::MockSigner;
+

+
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
    let size = fastrand::usize(range);
    let mut set = HashSet::with_capacity(size);
    let mut g = quickcheck::Gen::new(size);
@@ -20,35 +26,34 @@ pub fn set<T: Eq + Hash + quickcheck::Arbitrary>(range: impl RangeBounds<usize>)
    set
}

-
pub fn gen<T: quickcheck::Arbitrary>(size: usize) -> T {
+
pub fn gen<T: Arbitrary>(size: usize) -> T {
    let mut gen = quickcheck::Gen::new(size);

    T::arbitrary(&mut gen)
}

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

        storage::Remotes::new(remotes)
    }
}

-
impl quickcheck::Arbitrary for MockStorage {
+
impl Arbitrary for MockStorage {
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let inventory = quickcheck::Arbitrary::arbitrary(g);
+
        let inventory = Arbitrary::arbitrary(g);
        MockStorage::new(inventory)
    }
}

-
impl quickcheck::Arbitrary for storage::Remote<storage::Unverified> {
+
impl Arbitrary for Refs {
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
-
        let mut refs: HashMap<storage::BranchName, storage::Oid> = HashMap::with_hasher(rng.into());
+
        let mut refs: BTreeMap<storage::RefName, storage::Oid> = BTreeMap::new();
        let mut bytes: [u8; 20] = [0; 20];
+
        // TODO: Use refs other than branch names.
        let names = &["master", "dev", "feature/1", "feature/2", "feature/3"];
-
        let id = UserId::arbitrary(g);

        for _ in 0..g.size().min(2) {
            if let Some(name) = g.choose(names) {
@@ -59,25 +64,46 @@ impl quickcheck::Arbitrary for storage::Remote<storage::Unverified> {
                refs.insert(name.to_string(), oid);
            }
        }
-
        storage::Remote::new(id, refs)
+
        Self::from(refs)
+
    }
+
}
+

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

+
        storage::Remote::new(*signer.public_key(), signed)
+
    }
+
}
+

+
impl Arbitrary for MockSigner {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let mut bytes: [u8; 32] = [0; 32];
+

+
        for byte in &mut bytes {
+
            *byte = u8::arbitrary(g);
+
        }
+
        MockSigner::from(SecretKey::from(bytes))
    }
}

-
impl quickcheck::Arbitrary for ProjId {
+
impl Arbitrary for ProjId {
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
        let digest = hash::Digest::arbitrary(g);
        ProjId::from(digest)
    }
}

-
impl quickcheck::Arbitrary for hash::Digest {
+
impl Arbitrary for hash::Digest {
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: Vec<u8> = quickcheck::Arbitrary::arbitrary(g);
+
        let bytes: Vec<u8> = Arbitrary::arbitrary(g);
        hash::Digest::new(&bytes)
    }
}

-
impl quickcheck::Arbitrary for PublicKey {
+
impl Arbitrary for PublicKey {
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
        use ed25519_consensus::SigningKey;

modified node/src/test/crypto.rs
@@ -2,7 +2,7 @@ use ed25519_consensus as ed25519;

use crate::crypto::{PublicKey, SecretKey, Signer};

-
#[derive(Debug)]
+
#[derive(Debug, Clone)]
pub struct MockSigner {
    pk: PublicKey,
    sk: SecretKey,
@@ -15,12 +15,14 @@ impl MockSigner {
        for byte in &mut bytes {
            *byte = rng.u8(..);
        }
-
        let sk = SecretKey::from(bytes);
+
        Self::from(SecretKey::from(bytes))
+
    }
+
}

-
        Self {
-
            pk: sk.verification_key().into(),
-
            sk,
-
        }
+
impl From<SecretKey> for MockSigner {
+
    fn from(sk: SecretKey) -> Self {
+
        let pk = sk.verification_key().into();
+
        Self { sk, pk }
    }
}

@@ -36,6 +38,20 @@ impl Default for MockSigner {
    }
}

+
impl PartialEq for MockSigner {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.pk == other.pk
+
    }
+
}
+

+
impl Eq for MockSigner {}
+

+
impl std::hash::Hash for MockSigner {
+
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+
        self.pk.hash(state)
+
    }
+
}
+

impl Signer for MockSigner {
    fn public_key(&self) -> &PublicKey {
        &self.pk
modified node/src/test/fixtures.rs
@@ -1,59 +1,66 @@
use std::path::Path;

use crate::git;
-
use crate::identity::{ProjId, UserId};
+
use crate::identity::ProjId;
use crate::storage::git::Storage;
-
use crate::storage::WriteStorage;
+
use crate::storage::{ReadStorage, WriteStorage};
use crate::test::arbitrary;
use crate::test::crypto::MockSigner;

pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
    let path = path.as_ref();
-
    let storage = Storage::open(path, MockSigner::default()).unwrap();
    let proj_ids = arbitrary::set::<ProjId>(3..5);
-
    let user_ids = arbitrary::set::<UserId>(1..3);
+
    let signers = arbitrary::set::<MockSigner>(1..3);
+
    let mut storages = signers
+
        .into_iter()
+
        .map(|s| Storage::open(path, s).unwrap())
+
        .collect::<Vec<_>>();

    crate::test::logger::init(log::Level::Debug);

-
    for proj in proj_ids.iter() {
-
        log::debug!("creating {}...", proj);
-
        let repo = storage.repository(proj).unwrap();
+
    for storage in &storages {
+
        let remote = storage.user_id();

-
        for user in user_ids.iter() {
-
            let repo = &repo.backend;
-
            let sig = git2::Signature::now(&user.to_string(), "anonymous@radicle.xyz").unwrap();
-
            let head = git::initial_commit(repo, &sig).unwrap();
+
        log::debug!("signer {}...", remote);

-
            log::debug!("{}: creating {}...", proj, user);
+
        for proj in proj_ids.iter() {
+
            let repo = storage.repository(proj).unwrap();
+
            let raw = &repo.backend;
+
            let sig = git2::Signature::now(&remote.to_string(), "anonymous@radicle.xyz").unwrap();
+
            let head = git::initial_commit(raw, &sig).unwrap();

-
            repo.reference(
-
                &format!("refs/remotes/{user}/heads/radicle/id"),
+
            log::debug!("{}: creating {}...", remote, proj);
+

+
            raw.reference(
+
                &format!("refs/remotes/{remote}/heads/radicle/id"),
                head.id(),
                false,
                "test",
            )
            .unwrap();

-
            let head = git::commit(repo, &head, "Second commit", &user.to_string()).unwrap();
-
            repo.reference(
-
                &format!("refs/remotes/{user}/heads/master"),
+
            let head = git::commit(raw, &head, "Second commit", &remote.to_string()).unwrap();
+
            raw.reference(
+
                &format!("refs/remotes/{remote}/heads/master"),
                head.id(),
                false,
                "test",
            )
            .unwrap();

-
            let head = git::commit(repo, &head, "Third commit", &user.to_string()).unwrap();
-
            repo.reference(
-
                &format!("refs/remotes/{user}/heads/patch/3"),
+
            let head = git::commit(raw, &head, "Third commit", &remote.to_string()).unwrap();
+
            raw.reference(
+
                &format!("refs/remotes/{remote}/heads/patch/3"),
                head.id(),
                false,
                "test",
            )
            .unwrap();
+

+
            storage.sign_refs(&repo).unwrap();
        }
    }
-
    storage
+
    storages.pop().unwrap()
}

#[cfg(test)]
modified node/src/test/storage.rs
@@ -1,18 +1,19 @@
use git_url::Url;

+
use crate::crypto::Verified;
use crate::identity::{ProjId, UserId};
+
use crate::storage::refs;
use crate::storage::{
-
    Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, Unverified, WriteRepository,
-
    WriteStorage,
+
    Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, WriteRepository, WriteStorage,
};

#[derive(Clone, Debug)]
pub struct MockStorage {
-
    pub inventory: Vec<(ProjId, Remotes<Unverified>)>,
+
    pub inventory: Vec<(ProjId, Remotes<Verified>)>,
}

impl MockStorage {
-
    pub fn new(inventory: Vec<(ProjId, Remotes<Unverified>)>) -> Self {
+
    pub fn new(inventory: Vec<(ProjId, Remotes<Verified>)>) -> Self {
        Self { inventory }
    }

@@ -36,7 +37,7 @@ impl ReadStorage for MockStorage {
        }
    }

-
    fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
+
    fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
        if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) {
            return Ok(Some(refs.clone()));
        }
@@ -60,6 +61,13 @@ impl WriteStorage for MockStorage {
    fn repository(&self, _proj: &ProjId) -> Result<Self::Repository, Error> {
        Ok(MockRepository {})
    }
+

+
    fn sign_refs(
+
        &self,
+
        _repository: &Self::Repository,
+
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
+
        todo!()
+
    }
}

pub struct MockRepository {}
@@ -69,11 +77,39 @@ impl ReadRepository for MockRepository {
        todo!()
    }

-
    fn remote(&self, _user: &UserId) -> Result<Remote<Unverified>, Error> {
+
    fn remote(&self, _user: &UserId) -> Result<Remote<Verified>, refs::Error> {
+
        todo!()
+
    }
+

+
    fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
+
        todo!()
+
    }
+

+
    fn blob_at<'a>(
+
        &'a self,
+
        _oid: radicle_git_ext::Oid,
+
        _path: &'a std::path::Path,
+
    ) -> Result<git2::Blob<'a>, radicle_git_ext::Error> {
        todo!()
    }

-
    fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
+
    fn reference(
+
        &self,
+
        _user: &UserId,
+
        _reference: &str,
+
    ) -> Result<Option<git2::Reference>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn reference_oid(
+
        &self,
+
        _user: &UserId,
+
        _reference: &str,
+
    ) -> Result<Option<radicle_git_ext::Oid>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn references(&self, _user: &UserId) -> Result<crate::storage::refs::Refs, Error> {
        todo!()
    }
}
@@ -82,4 +118,8 @@ impl WriteRepository for MockRepository {
    fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
        Ok(())
    }
+

+
    fn raw(&self) -> &git2::Repository {
+
        todo!()
+
    }
}